osherr commited on
Commit
3bbfbf5
Β·
verified Β·
1 Parent(s): 20f74b5

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +52 -44
src/streamlit_app.py CHANGED
@@ -28,19 +28,15 @@ if current_dir not in sys.path:
28
  def load_backbones(repo_id, dav_file, dino_file, m_size):
29
  device = "cuda" if torch.cuda.is_available() else "cpu"
30
 
31
- # 1. Load Depth Anything V2 with error handling for config
32
  dav_path = hf_hub_download(repo_id=repo_id, filename=dav_file)
33
  from depth_anything_v2.dpt import DepthAnythingV2
34
-
35
- # Manual config matching the expected ViT architecture
36
- model_configs = {
37
  'vits': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]},
38
  'vitb': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]},
39
  'vitl': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]},
40
  }
41
-
42
- # Initialize without the problematic 'encoder_cond_dim'
43
- dav_model = DepthAnythingV2(**model_configs[m_size])
44
  dav_model.load_state_dict(torch.load(dav_path, map_location="cpu", weights_only=True))
45
  dav_model = dav_model.to(device).eval()
46
 
@@ -60,10 +56,12 @@ def load_backbones(repo_id, dav_file, dino_file, m_size):
60
  return dav_model, dino_model, device
61
 
62
  def extract_dino_features(encoder, img_tensor, device):
63
- """Extracts patch tokens and computes PCA map [3, H, W] using sigmoid(2z) style."""
64
  B, C, H, W = img_tensor.shape
65
- x = (img_tensor - torch.tensor(IMAGENET_MEAN, device=device).view(1,3,1,1)) / \
66
- torch.tensor(IMAGENET_STD, device=device).view(1,3,1,1)
 
 
67
 
68
  with torch.no_grad():
69
  pe = encoder.patch_embed(x)
@@ -74,7 +72,6 @@ def extract_dino_features(encoder, img_tensor, device):
74
  tokens = tokens + encoder.interpolate_pos_encoding(tokens, H, W)
75
  for blk in encoder.blocks: tokens = blk(tokens)
76
  if hasattr(encoder, "norm"): tokens = encoder.norm(tokens)
77
-
78
  patch_tokens = tokens[:, 1:, :].squeeze(0).cpu().numpy()
79
 
80
  pca = PCA(n_components=3, whiten=True)
@@ -85,12 +82,12 @@ def extract_dino_features(encoder, img_tensor, device):
85
  norm_features = patch_tokens / (np.linalg.norm(patch_tokens, axis=1, keepdims=True) + 1e-6)
86
  return norm_features, Z_rgb.transpose(2, 0, 1), (Hp, Wp)
87
 
88
- # ================= UI DESIGN =================
89
- st.set_page_config(layout="wide", page_title="Prior2DSM Pro", page_icon="πŸ›°οΈ")
90
 
91
  st.markdown("""
92
  <style>
93
- .stTabs [data-baseweb="tab"] { background-color: #f0f2f6; border-radius: 4px; padding: 10px; }
94
  .stTabs [aria-selected="true"] { background-color: #2e7d32; color: white; }
95
  .stButton>button { border-radius: 8px; background-color: #2e7d32; color: white; width: 100%; height: 3.5em; }
96
  </style>
@@ -107,18 +104,18 @@ with st.sidebar:
107
  prior_file = st.file_uploader("Prior DSM (TIF)", type=["tif", "tiff"])
108
 
109
  st.divider()
110
- st.header("βš™οΈ Configuration")
111
- m_size = st.selectbox("DAV2 Size", ["vitl", "vitb", "vits"])
112
- repo = st.text_input("Repo", "osherr/Prior2DSM")
113
- dav_ckpt = st.text_input("DAV2 Weights", "depth_anything_v2_vitl.pth")
114
- dino_ckpt = st.text_input("DINO Weights", "dinov3_vitl16_pretrain_sat493m-eadcf0ff.pth")
115
 
116
  if rgb_file and prior_file:
117
  dav_model, dino_model, device = load_backbones(repo, dav_ckpt, dino_ckpt, m_size)
118
 
119
  with rasterio.open(BytesIO(rgb_file.read())) as src:
120
  n_bands = src.count
121
- sel_bands = st.sidebar.multiselect("RGB Bands", range(1, n_bands+1), default=[1, 2, 3] if n_bands >=3 else [1])
122
  rgb_raw = src.read(sel_bands)
123
 
124
  with rasterio.open(BytesIO(prior_file.read())) as src:
@@ -126,69 +123,80 @@ if rgb_file and prior_file:
126
  p_meta = src.profile
127
  h_f, w_f = p_meta['height'], p_meta['width']
128
 
129
- # Inference Prep
 
130
  img_448 = cv2.resize(rgb_raw[:3].transpose(1, 2, 0), (448, 448))
131
  img_1024 = cv2.resize(rgb_raw[:3].transpose(1, 2, 0), (1024, 1024))
132
 
133
- with st.spinner("Processing Backbones..."):
134
- dav_in = torch.from_numpy(img_448).transpose(2, 0, 1).unsqueeze(0).to(device).float() / 255.0
 
135
  with torch.no_grad():
136
  rel_depth = dav_model(dav_in)
137
  if isinstance(rel_depth, (list, tuple)): rel_depth = rel_depth[-1]
138
  rel_depth = F.interpolate(rel_depth.unsqueeze(1), size=(h_f, w_f), mode="bilinear").squeeze().cpu().numpy()
139
 
140
- dino_in = torch.from_numpy(img_1024).transpose(2, 0, 1).unsqueeze(0).to(device).float() / 255.0
141
- _, pca_raw, _ = extract_dino_features(dino_model, dino_in, device)
142
  pca_viz = F.interpolate(torch.from_numpy(pca_raw).unsqueeze(0), size=(h_f, w_f), mode="bilinear").squeeze().numpy()
143
 
144
- # UI Tabs
145
- tab_mask, tab_diag, tab_refine = st.tabs(["πŸ–ŒοΈ Masking", "πŸ”¬ Diagnostics", "πŸ—οΈ Refinement"])
146
 
147
  with tab_mask:
 
148
  viz_rgb = np.clip(rgb_raw[:3].transpose(1, 2, 0) / (np.percentile(rgb_raw[:3], 98) + 1e-6), 0, 1)
149
  viz_small = (cv2.resize(viz_rgb, (640, 640)) * 255).astype(np.uint8)
150
- col_m1, col_m2 = st.columns([2, 1])
151
  with col_m1:
 
152
  coords = streamlit_image_coordinates(viz_small, key="mask_painter")
153
  if coords:
154
  st.session_state.points.append((int(coords['x']*(w_f/640)), int(coords['y']*(h_f/640))))
155
  st.rerun()
156
  with col_m2:
157
- if st.button("πŸ—‘οΈ Clear Mask"):
158
  st.session_state.points = []
159
  st.rerun()
160
- st.write(f"Points: {len(st.session_state.points)}")
161
 
 
162
  mask = np.zeros((h_f, w_f), dtype=bool)
163
  if len(st.session_state.points) >= 2:
164
  for i in range(0, len(st.session_state.points)-1, 2):
165
  p1, p2 = st.session_state.points[i], st.session_state.points[i+1]
166
  mask[min(p1[1], p2[1]):max(p1[1], p2[1]), min(p1[0], p2[0]):max(p1[0], p2[0])] = True
167
 
 
 
 
 
 
 
 
168
  with tab_refine:
169
- if st.button("πŸš€ Start Fine Refinement"):
170
- with st.spinner("Injecting Fine Laplacian Details..."):
171
- # Move to device once
172
- D_dav = torch.from_numpy(rel_depth).to(device).view(1, 1, h_f, w_f)
173
- D_prior = torch.from_numpy(prior_data).to(device).view(1, 1, h_f, w_f)
174
- Anchor = torch.from_numpy((~mask).astype(np.float32)).to(device).view(1, 1, h_f, w_f)
175
 
176
  D_opt = D_prior.clone().detach().requires_grad_(True)
177
  optimizer = torch.optim.Adam([D_opt], lr=8e-3)
178
-
179
- # Align Global Scale
180
  D_dav_s = ((D_dav - D_dav.mean()) / D_dav.std()) * D_prior.std() + D_prior.mean()
181
 
182
  for _ in range(300):
183
  optimizer.zero_grad()
184
- # Loss 1: Anchor constraint (don't move valid ground)
185
  l_anchor = F.mse_loss(D_opt * Anchor, D_prior * Anchor)
186
- # Loss 2: Laplacian Detail (transfer high-frequency features)
187
  l_geom = F.l1_loss(D_opt - F.avg_pool2d(D_opt, 5, 1, 2),
188
  D_dav_s - F.avg_pool2d(D_dav_s, 5, 1, 2))
189
-
190
- (1500 * l_anchor + 250 * l_geom).backward()
191
  optimizer.step()
192
-
 
 
 
 
 
 
193
  else:
194
- st.info("Upload TIF/PNG assets to begin.")
 
28
  def load_backbones(repo_id, dav_file, dino_file, m_size):
29
  device = "cuda" if torch.cuda.is_available() else "cpu"
30
 
31
+ # 1. Load Depth Anything V2 (Manual Init)
32
  dav_path = hf_hub_download(repo_id=repo_id, filename=dav_file)
33
  from depth_anything_v2.dpt import DepthAnythingV2
34
+ configs = {
 
 
35
  'vits': {'encoder': 'vits', 'features': 64, 'out_channels': [48, 96, 192, 384]},
36
  'vitb': {'encoder': 'vitb', 'features': 128, 'out_channels': [96, 192, 384, 768]},
37
  'vitl': {'encoder': 'vitl', 'features': 256, 'out_channels': [256, 512, 1024, 1024]},
38
  }
39
+ dav_model = DepthAnythingV2(**configs[m_size])
 
 
40
  dav_model.load_state_dict(torch.load(dav_path, map_location="cpu", weights_only=True))
41
  dav_model = dav_model.to(device).eval()
42
 
 
56
  return dav_model, dino_model, device
57
 
58
  def extract_dino_features(encoder, img_tensor, device):
59
+ """CHW input tensor -> normalized features and PCA map."""
60
  B, C, H, W = img_tensor.shape
61
+ # Standardize
62
+ mean = torch.tensor(IMAGENET_MEAN, device=device).view(1, 3, 1, 1)
63
+ std = torch.tensor(IMAGENET_STD, device=device).view(1, 3, 1, 1)
64
+ x = (img_tensor - mean) / std
65
 
66
  with torch.no_grad():
67
  pe = encoder.patch_embed(x)
 
72
  tokens = tokens + encoder.interpolate_pos_encoding(tokens, H, W)
73
  for blk in encoder.blocks: tokens = blk(tokens)
74
  if hasattr(encoder, "norm"): tokens = encoder.norm(tokens)
 
75
  patch_tokens = tokens[:, 1:, :].squeeze(0).cpu().numpy()
76
 
77
  pca = PCA(n_components=3, whiten=True)
 
82
  norm_features = patch_tokens / (np.linalg.norm(patch_tokens, axis=1, keepdims=True) + 1e-6)
83
  return norm_features, Z_rgb.transpose(2, 0, 1), (Hp, Wp)
84
 
85
+ # ================= UI =================
86
+ st.set_page_config(layout="wide", page_title="Prior2DSM Pro")
87
 
88
  st.markdown("""
89
  <style>
90
+ .stTabs [data-baseweb="tab"] { background-color: #f0f2f6; border-radius: 4px; padding: 10px; font-weight: bold; }
91
  .stTabs [aria-selected="true"] { background-color: #2e7d32; color: white; }
92
  .stButton>button { border-radius: 8px; background-color: #2e7d32; color: white; width: 100%; height: 3.5em; }
93
  </style>
 
104
  prior_file = st.file_uploader("Prior DSM (TIF)", type=["tif", "tiff"])
105
 
106
  st.divider()
107
+ st.header("βš™οΈ Settings")
108
+ repo = st.text_input("Repo ID", "osherr/Prior2DSM")
109
+ dav_ckpt = st.text_input("DAV2 Filename", "depth_anything_v2_vitl.pth")
110
+ dino_ckpt = st.text_input("DINO Filename", "dinov3_vitl16_pretrain_sat493m-eadcf0ff.pth")
111
+ m_size = st.selectbox("DAV2 Encoder", ["vitl", "vitb", "vits"])
112
 
113
  if rgb_file and prior_file:
114
  dav_model, dino_model, device = load_backbones(repo, dav_ckpt, dino_ckpt, m_size)
115
 
116
  with rasterio.open(BytesIO(rgb_file.read())) as src:
117
  n_bands = src.count
118
+ sel_bands = st.sidebar.multiselect("Select RGB Bands", range(1, n_bands+1), default=[1, 2, 3] if n_bands >=3 else [1])
119
  rgb_raw = src.read(sel_bands)
120
 
121
  with rasterio.open(BytesIO(prior_file.read())) as src:
 
123
  p_meta = src.profile
124
  h_f, w_f = p_meta['height'], p_meta['width']
125
 
126
+ # --- PREPARE INPUTS ---
127
+ # Convert HWC (OpenCV/NumPy) to CHW (PyTorch) using .permute() instead of .transpose()
128
  img_448 = cv2.resize(rgb_raw[:3].transpose(1, 2, 0), (448, 448))
129
  img_1024 = cv2.resize(rgb_raw[:3].transpose(1, 2, 0), (1024, 1024))
130
 
131
+ with st.spinner("Processing Neural Backbones..."):
132
+ # Fix: Use .permute(2, 0, 1) for reordering 3 dimensions
133
+ dav_in = torch.from_numpy(img_448).permute(2, 0, 1).unsqueeze(0).to(device).float() / 255.0
134
  with torch.no_grad():
135
  rel_depth = dav_model(dav_in)
136
  if isinstance(rel_depth, (list, tuple)): rel_depth = rel_depth[-1]
137
  rel_depth = F.interpolate(rel_depth.unsqueeze(1), size=(h_f, w_f), mode="bilinear").squeeze().cpu().numpy()
138
 
139
+ dino_in = torch.from_numpy(img_1024).permute(2, 0, 1).unsqueeze(0).to(device).float() / 255.0
140
+ dino_feats, pca_raw, _ = extract_dino_features(dino_model, dino_in, device)
141
  pca_viz = F.interpolate(torch.from_numpy(pca_raw).unsqueeze(0), size=(h_f, w_f), mode="bilinear").squeeze().numpy()
142
 
143
+ tab_mask, tab_diag, tab_refine = st.tabs(["πŸ–ŒοΈ Interactive Mask", "πŸ”¬ Diagnostics", "πŸ—οΈ Refinement"])
 
144
 
145
  with tab_mask:
146
+ col_m1, col_m2 = st.columns([2, 1])
147
  viz_rgb = np.clip(rgb_raw[:3].transpose(1, 2, 0) / (np.percentile(rgb_raw[:3], 98) + 1e-6), 0, 1)
148
  viz_small = (cv2.resize(viz_rgb, (640, 640)) * 255).astype(np.uint8)
149
+
150
  with col_m1:
151
+ st.write("Draw rectangles to mask change areas.")
152
  coords = streamlit_image_coordinates(viz_small, key="mask_painter")
153
  if coords:
154
  st.session_state.points.append((int(coords['x']*(w_f/640)), int(coords['y']*(h_f/640))))
155
  st.rerun()
156
  with col_m2:
157
+ if st.button("πŸ—‘οΈ Reset Mask"):
158
  st.session_state.points = []
159
  st.rerun()
160
+ st.metric("Points", len(st.session_state.points))
161
 
162
+ # Mask construction
163
  mask = np.zeros((h_f, w_f), dtype=bool)
164
  if len(st.session_state.points) >= 2:
165
  for i in range(0, len(st.session_state.points)-1, 2):
166
  p1, p2 = st.session_state.points[i], st.session_state.points[i+1]
167
  mask[min(p1[1], p2[1]):max(p1[1], p2[1]), min(p1[0], p2[0]):max(p1[0], p2[0])] = True
168
 
169
+ with tab_diag:
170
+ if st.button("Generate Diagnostic PCA View"):
171
+ fig, ax = plt.subplots()
172
+ pca_rgb_view = 1.0 / (1.0 + np.exp(-2.0 * pca_viz.transpose(1, 2, 0)))
173
+ ax.imshow(pca_rgb_view)
174
+ st.pyplot(fig)
175
+
176
  with tab_refine:
177
+ if st.button("πŸš€ Run Fine Refinement"):
178
+ with st.spinner("Refining Terrain..."):
179
+ D_dav = torch.from_numpy(rel_depth)[None, None].to(device)
180
+ D_prior = torch.from_numpy(prior_data)[None, None].to(device)
181
+ Anchor = torch.from_numpy((~mask).astype(np.float32))[None, None].to(device)
 
182
 
183
  D_opt = D_prior.clone().detach().requires_grad_(True)
184
  optimizer = torch.optim.Adam([D_opt], lr=8e-3)
 
 
185
  D_dav_s = ((D_dav - D_dav.mean()) / D_dav.std()) * D_prior.std() + D_prior.mean()
186
 
187
  for _ in range(300):
188
  optimizer.zero_grad()
 
189
  l_anchor = F.mse_loss(D_opt * Anchor, D_prior * Anchor)
 
190
  l_geom = F.l1_loss(D_opt - F.avg_pool2d(D_opt, 5, 1, 2),
191
  D_dav_s - F.avg_pool2d(D_dav_s, 5, 1, 2))
192
+ (1200 * l_anchor + 180 * l_geom).backward()
 
193
  optimizer.step()
194
+
195
+ res = D_opt.detach().squeeze().cpu().numpy()
196
+ buf = BytesIO()
197
+ p_meta.update(count=1, dtype="float32")
198
+ with rasterio.open(buf, "w", **p_meta) as dst: dst.write(res, 1)
199
+ st.success("Refinement Complete!")
200
+ st.download_button("πŸ“₯ Download Refined DSM", buf.getvalue(), "refined_fine.tif")
201
  else:
202
+ st.info("Upload your assets to begin.")