osherr commited on
Commit
ee3bfb9
Β·
verified Β·
1 Parent(s): 57b6e2a

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +53 -94
src/streamlit_app.py CHANGED
@@ -18,12 +18,9 @@ from PIL import Image
18
  # --- RESEARCH CONFIG ---
19
  IMAGENET_MEAN = (0.485, 0.456, 0.406)
20
  IMAGENET_STD = (0.229, 0.224, 0.225)
21
- current_dir = os.path.dirname(os.path.abspath(__file__))
22
- if current_dir not in sys.path:
23
- sys.path.append(current_dir)
24
 
25
  # ================= PAGE CONFIG & STATE INIT =================
26
- st.set_page_config(layout="wide", page_title="Prior2DSM Pro")
27
 
28
  if 'bbox_points' not in st.session_state:
29
  st.session_state.bbox_points = []
@@ -46,71 +43,26 @@ def load_backbones(repo_id, dav_file, dino_file, m_size):
46
  from dinov3.models.vision_transformer import DinoVisionTransformer
47
  dino_model = DinoVisionTransformer(img_size=1024, patch_size=16, embed_dim=1024, depth=24, num_heads=16, qkv_bias=True).to(device).eval()
48
  ckpt = torch.load(dino_path, map_location="cpu")
49
- for key in ["state_dict", "model", "teacher"]:
50
- if key in ckpt: ckpt = ckpt[key]
 
51
  clean_ckpt = {k.replace("module.", "").replace("backbone.", "").replace("teacher.backbone.", ""): v for k, v in ckpt.items()}
52
  dino_model.load_state_dict(clean_ckpt, strict=False)
53
- return dav_model, dino_model, device
54
-
55
- @torch.no_grad()
56
- def extract_dino_features_safe(encoder, x):
57
- """Ultimate robustness check for CLS/Patch token concatenation."""
58
- B, _, H, W = x.shape
59
- D = int(getattr(encoder, "embed_dim")) # 1024
60
-
61
- # Get patch embeddings
62
- pe = encoder.patch_embed(x)
63
-
64
- # Robust shape handling
65
- if pe.ndim == 4: # (B, C_out, Hp, Wp)
66
- Hp, Wp = pe.shape[-2], pe.shape[-1]
67
- tokens = pe.flatten(2).transpose(1, 2)
68
- else: # (B, N, C_out)
69
- Hp, Wp = H // 16, W // 16
70
- tokens = pe
71
-
72
- # CRITICAL FIX: If patch_embed didn't project to 1024 yet (e.g., returned 64)
73
- if tokens.shape[-1] != D:
74
- # Check for a projection layer often found in custom ViTs
75
- if hasattr(encoder.patch_embed, 'proj'):
76
- # If proj exists but wasn't used, we skip and use blocks which handle it
77
- pass
78
- # Emergency reshape/pad or block pass
79
- # In DINO ViT-L, the first block usually expects 1024.
80
- # If we have 64, it's because we are looking at raw conv outputs.
81
- # Let's use the internal forward_features if available or mimic it:
82
- tokens = encoder.prepare_tokens_with_masks(x) if hasattr(encoder, "prepare_tokens_with_masks") else None
83
-
84
- if tokens is None:
85
- # Manual fallback
86
- cls_token = encoder.cls_token.expand(B, -1, -1)
87
- # Re-derive tokens correctly if first attempt failed
88
- pe = encoder.patch_embed(x)
89
- tokens = pe.flatten(2).transpose(1, 2)
90
- # If still 64, we must use the encoder's own logic to get to 1024
91
- # Most DINOv3 models use a linear layer inside patch_embed
92
- tokens = torch.cat((cls_token, tokens), dim=1) # This is where it fails if tokens is (N, 64)
93
 
94
- # --- The standard research path from your notebook ---
95
- tokens = encoder.prepare_tokens_with_masks(x) # This is the safest way in DINOv3
96
- for blk in encoder.blocks:
97
- tokens = blk(tokens)
98
- if hasattr(encoder, "norm"):
99
- tokens = encoder.norm(tokens)
100
-
101
- patch_tokens = tokens[:, 1:, :].squeeze(0).cpu().numpy()
102
- return patch_tokens, H // 16, W // 16
103
 
104
- # ================= MAIN APP =================
105
  with st.sidebar:
106
  st.header("πŸ“‚ Data Assets")
107
  rgb_file = st.file_uploader("Source RGB (TIF)", type=["tif", "tiff"])
108
  prior_file = st.file_uploader("Prior DSM (TIF)", type=["tif", "tiff"])
 
109
  repo = st.text_input("Repo ID", "osherr/Prior2DSM")
110
  dav_ckpt = st.text_input("DAV2 Weights", "depth_anything_v2_vitl.pth")
111
  dino_ckpt = st.text_input("DINO Weights", "dinov3_vitl16_pretrain_sat493m-eadcf0ff.pth")
112
- m_size = st.selectbox("DAV2 Encoder", ["vitl", "vitb", "vits"])
113
 
 
114
  if rgb_file and prior_file:
115
  dav_model, dino_model, device = load_backbones(repo, dav_ckpt, dino_ckpt, m_size)
116
 
@@ -121,42 +73,48 @@ if rgb_file and prior_file:
121
  prior_data = src.read(1).astype(np.float32)
122
  h_f, w_f = prior_data.shape
123
 
124
- # PRE-PROCESSING
125
  img_448 = cv2.resize(rgb_raw.transpose(1, 2, 0), (448, 448))
126
  img_1024 = cv2.resize(rgb_raw.transpose(1, 2, 0), (1024, 1024))
127
 
128
  with st.spinner("Processing Backbones..."):
129
- # 1. Depth (448px)
130
  dav_in = torch.from_numpy(img_448).permute(2, 0, 1).unsqueeze(0).to(device).float() / 255.0
131
  with torch.no_grad():
132
  rel_depth = dav_model(dav_in)
133
  if isinstance(rel_depth, (list, tuple)): rel_depth = rel_depth[-1]
134
  rel_depth = F.interpolate(rel_depth.unsqueeze(1), size=(h_f, w_f), mode="bilinear").squeeze().cpu().numpy()
135
 
136
- # 2. DINO Features (1024px)
137
  dino_in = (torch.from_numpy(img_1024).permute(2, 0, 1).unsqueeze(0).to(device).float() / 255.0 - torch.tensor(IMAGENET_MEAN, device=device).view(1,3,1,1)) / torch.tensor(IMAGENET_STD, device=device).view(1,3,1,1)
138
 
139
- # USE THE ENCODER'S INTERNAL METHOD TO AVOID CONCAT ERRORS
140
  with torch.no_grad():
141
- # DINOv3 standard: forward_features or prepare_tokens_with_masks
142
- tokens = dino_model.prepare_tokens_with_masks(dino_in)
143
- for blk in dino_model.blocks: tokens = blk(tokens)
144
- if hasattr(dino_model, "norm"): tokens = dino_model.norm(tokens)
 
 
 
 
 
145
  patch_tokens = tokens[:, 1:, :].squeeze(0).cpu().numpy()
146
  Hp, Wp = 1024 // 16, 1024 // 16
147
 
148
- # PCA Style from Notebook
149
  pca = PCA(n_components=3, whiten=True)
150
  Z = pca.fit_transform(patch_tokens).reshape(Hp, Wp, 3)
 
151
  Z_rgb = 1.0 / (1.0 + np.exp(-2.0 * Z))
152
  pca_full = F.interpolate(torch.from_numpy(Z_rgb.transpose(2,0,1)).unsqueeze(0), size=(h_f, w_f), mode="bilinear").squeeze().numpy()
153
 
154
- # --- TABS ---
155
- tab_point, tab_refine = st.tabs(["πŸ”¬ Point Diagnostic", "πŸš€ BBOX Refinement"])
 
 
 
156
 
157
  with tab_point:
158
- viz_rgb = np.clip(rgb_raw.transpose(1, 2, 0) / (np.percentile(rgb_raw, 98) + 1e-6), 0, 1)
159
- viz_small = (cv2.resize(viz_rgb, (640, 640)) * 255).astype(np.uint8)
160
  p_diag = streamlit_image_coordinates(viz_small, key="point_diag")
161
  if p_diag:
162
  tx, ty = int(p_diag['x']*(w_f/640)), int(p_diag['y']*(h_f/640))
@@ -170,7 +128,7 @@ if rgb_file and prior_file:
170
  k_rel, k_elev = c_rel[np.argsort(spatial_dists)[:60]], c_elev[np.argsort(spatial_dists)[:60]]
171
  reg = LinearRegression().fit(k_rel.reshape(-1, 1), k_elev)
172
  pred_h = reg.predict([[rel_depth[ty, tx]]])[0]
173
- st.metric("Estimated Local Elevation", f"{pred_h:.2f} m")
174
  fig, ax = plt.subplots(); ax.scatter(k_rel, k_elev, color='lawngreen'); ax.plot(k_rel, reg.predict(k_rel.reshape(-1,1)), 'b-'); st.pyplot(fig)
175
 
176
  with tab_refine:
@@ -180,28 +138,29 @@ if rgb_file and prior_file:
180
  st.rerun()
181
  if len(st.session_state.bbox_points) >= 2:
182
  if st.button("πŸš€ Start 4-Stage Optimization"):
183
- mask = np.zeros((h_f, w_f), dtype=bool)
184
- for i in range(0, len(st.session_state.bbox_points)-1, 2):
185
- p1, p2 = st.session_state.bbox_points[i], st.session_state.bbox_points[i+1]
186
- mask[min(p1[1], p2[1]):max(p1[1], p2[1]), min(p1[0], p2[0]):max(p1[0], p2[0])] = True
187
-
188
- # STAGE 3 & 4: Optimization
189
- D_dav_t = torch.from_numpy(rel_depth)[None, None].to(device)
190
- D_prior_t = torch.from_numpy(np.nan_to_num(prior_data))[None, None].to(device)
191
- Anchor = torch.from_numpy((~mask).astype(np.float32))[None, None].to(device)
192
- D_opt = D_prior_t.clone().detach().requires_grad_(True)
193
- opt = torch.optim.Adam([D_opt], lr=8e-3)
194
- D_dav_s = ((D_dav_t - D_dav_t.mean()) / D_dav_t.std()) * D_prior_t.std() + D_prior_t.mean()
195
-
196
- for _ in range(300):
197
- opt.zero_grad()
198
- l_a = F.mse_loss(D_opt * Anchor, D_prior_t * Anchor)
199
- l_l = F.l1_loss(D_opt - F.avg_pool2d(D_opt, 5, 1, 2), D_dav_s - F.avg_pool2d(D_dav_s, 5, 1, 2))
200
- (1200 * l_a + 180 * l_l).backward(); opt.step()
201
-
202
- st.success("Optimization Complete.")
203
- buf = BytesIO(); meta_full.update(count=1, dtype="float32")
204
- with rasterio.open(buf, "w", **meta_full) as dst: dst.write(D_opt.detach().squeeze().cpu().numpy(), 1)
205
- st.download_button("πŸ“₯ Download Refined DSM", buf.getvalue(), "refined.tif")
 
206
  else:
207
  st.info("Upload Assets to begin.")
 
18
  # --- RESEARCH CONFIG ---
19
  IMAGENET_MEAN = (0.485, 0.456, 0.406)
20
  IMAGENET_STD = (0.229, 0.224, 0.225)
 
 
 
21
 
22
  # ================= PAGE CONFIG & STATE INIT =================
23
+ st.set_page_config(layout="wide", page_title="Prior2DSM Pro", page_icon="πŸ›°οΈ")
24
 
25
  if 'bbox_points' not in st.session_state:
26
  st.session_state.bbox_points = []
 
43
  from dinov3.models.vision_transformer import DinoVisionTransformer
44
  dino_model = DinoVisionTransformer(img_size=1024, patch_size=16, embed_dim=1024, depth=24, num_heads=16, qkv_bias=True).to(device).eval()
45
  ckpt = torch.load(dino_path, map_location="cpu")
46
+ if "state_dict" in ckpt: ckpt = ckpt["state_dict"]
47
+ elif "model" in ckpt: ckpt = ckpt["model"]
48
+ elif "teacher" in ckpt: ckpt = ckpt["teacher"]
49
  clean_ckpt = {k.replace("module.", "").replace("backbone.", "").replace("teacher.backbone.", ""): v for k, v in ckpt.items()}
50
  dino_model.load_state_dict(clean_ckpt, strict=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
51
 
52
+ return dav_model, dino_model, device
 
 
 
 
 
 
 
 
53
 
54
+ # ================= SIDEBAR =================
55
  with st.sidebar:
56
  st.header("πŸ“‚ Data Assets")
57
  rgb_file = st.file_uploader("Source RGB (TIF)", type=["tif", "tiff"])
58
  prior_file = st.file_uploader("Prior DSM (TIF)", type=["tif", "tiff"])
59
+ st.divider()
60
  repo = st.text_input("Repo ID", "osherr/Prior2DSM")
61
  dav_ckpt = st.text_input("DAV2 Weights", "depth_anything_v2_vitl.pth")
62
  dino_ckpt = st.text_input("DINO Weights", "dinov3_vitl16_pretrain_sat493m-eadcf0ff.pth")
63
+ m_size = st.selectbox("DAV2 Size", ["vitl", "vitb", "vits"])
64
 
65
+ # ================= MAIN APP =================
66
  if rgb_file and prior_file:
67
  dav_model, dino_model, device = load_backbones(repo, dav_ckpt, dino_ckpt, m_size)
68
 
 
73
  prior_data = src.read(1).astype(np.float32)
74
  h_f, w_f = prior_data.shape
75
 
76
+ # Pre-processing at specific grid resolutions
77
  img_448 = cv2.resize(rgb_raw.transpose(1, 2, 0), (448, 448))
78
  img_1024 = cv2.resize(rgb_raw.transpose(1, 2, 0), (1024, 1024))
79
 
80
  with st.spinner("Processing Backbones..."):
81
+ # 1. Depth (DAV2)
82
  dav_in = torch.from_numpy(img_448).permute(2, 0, 1).unsqueeze(0).to(device).float() / 255.0
83
  with torch.no_grad():
84
  rel_depth = dav_model(dav_in)
85
  if isinstance(rel_depth, (list, tuple)): rel_depth = rel_depth[-1]
86
  rel_depth = F.interpolate(rel_depth.unsqueeze(1), size=(h_f, w_f), mode="bilinear").squeeze().cpu().numpy()
87
 
88
+ # 2. DINO Features (DINOv3) - Using high-level forward_features to avoid AssertionErrors
89
  dino_in = (torch.from_numpy(img_1024).permute(2, 0, 1).unsqueeze(0).to(device).float() / 255.0 - torch.tensor(IMAGENET_MEAN, device=device).view(1,3,1,1)) / torch.tensor(IMAGENET_STD, device=device).view(1,3,1,1)
90
 
 
91
  with torch.no_grad():
92
+ # forward_features handles RoPE and masks correctly
93
+ features = dino_model.forward_features(dino_in)
94
+ if isinstance(features, (list, tuple)):
95
+ tokens = features[0] # Typical DINOv3 returns (tokens, metadata)
96
+ else:
97
+ tokens = features
98
+
99
+ # Extract only the patch tokens (ignoring CLS/Registers)
100
+ # DINOv3 often uses 1 CLS + N patches
101
  patch_tokens = tokens[:, 1:, :].squeeze(0).cpu().numpy()
102
  Hp, Wp = 1024 // 16, 1024 // 16
103
 
104
+ # PCA Generation
105
  pca = PCA(n_components=3, whiten=True)
106
  Z = pca.fit_transform(patch_tokens).reshape(Hp, Wp, 3)
107
+ # Notebook Sigmoid 2*Z Style
108
  Z_rgb = 1.0 / (1.0 + np.exp(-2.0 * Z))
109
  pca_full = F.interpolate(torch.from_numpy(Z_rgb.transpose(2,0,1)).unsqueeze(0), size=(h_f, w_f), mode="bilinear").squeeze().numpy()
110
 
111
+ # --- UI ---
112
+ tab_point, tab_refine = st.tabs(["πŸ”¬ Point Diagnostic (Coarse)", "πŸš€ BBOX Refinement (Full)"])
113
+
114
+ viz_rgb = np.clip(rgb_raw.transpose(1, 2, 0) / (np.percentile(rgb_raw, 98) + 1e-6), 0, 1)
115
+ viz_small = (cv2.resize(viz_rgb, (640, 640)) * 255).astype(np.uint8)
116
 
117
  with tab_point:
 
 
118
  p_diag = streamlit_image_coordinates(viz_small, key="point_diag")
119
  if p_diag:
120
  tx, ty = int(p_diag['x']*(w_f/640)), int(p_diag['y']*(h_f/640))
 
128
  k_rel, k_elev = c_rel[np.argsort(spatial_dists)[:60]], c_elev[np.argsort(spatial_dists)[:60]]
129
  reg = LinearRegression().fit(k_rel.reshape(-1, 1), k_elev)
130
  pred_h = reg.predict([[rel_depth[ty, tx]]])[0]
131
+ st.metric("Estimated Elevation (Coarse)", f"{pred_h:.2f} m")
132
  fig, ax = plt.subplots(); ax.scatter(k_rel, k_elev, color='lawngreen'); ax.plot(k_rel, reg.predict(k_rel.reshape(-1,1)), 'b-'); st.pyplot(fig)
133
 
134
  with tab_refine:
 
138
  st.rerun()
139
  if len(st.session_state.bbox_points) >= 2:
140
  if st.button("πŸš€ Start 4-Stage Optimization"):
141
+ with st.spinner("Processing Stage 3 (Gradient) & Stage 4 (SSI)..."):
142
+ mask = np.zeros((h_f, w_f), dtype=bool)
143
+ for i in range(0, len(st.session_state.bbox_points)-1, 2):
144
+ p1, p2 = st.session_state.bbox_points[i], st.session_state.bbox_points[i+1]
145
+ mask[min(p1[1], p2[1]):max(p1[1], p2[1]), min(p1[0], p2[0]):max(p1[0], p2[0])] = True
146
+
147
+ D_dav_t = torch.from_numpy(rel_depth)[None, None].to(device)
148
+ D_prior_t = torch.from_numpy(np.nan_to_num(prior_data))[None, None].to(device)
149
+ Anchor = torch.from_numpy((~mask).astype(np.float32))[None, None].to(device)
150
+ D_opt = D_prior_t.clone().detach().requires_grad_(True)
151
+ opt = torch.optim.Adam([D_opt], lr=8e-3)
152
+ D_dav_s = ((D_dav_t - D_dav_t.mean()) / D_dav_t.std()) * D_prior_t.std() + D_prior_t.mean()
153
+
154
+ for _ in range(300):
155
+ opt.zero_grad()
156
+ l_a = F.mse_loss(D_opt * Anchor, D_prior_t * Anchor)
157
+ l_l = F.l1_loss(D_opt - F.avg_pool2d(D_opt, 5, 1, 2), D_dav_s - F.avg_pool2d(D_dav_s, 5, 1, 2))
158
+ (1500 * l_a + 250 * l_l).backward(); opt.step()
159
+
160
+ res = D_opt.detach().squeeze().cpu().numpy()
161
+ buf = BytesIO(); meta_full.update(count=1, dtype="float32")
162
+ with rasterio.open(buf, "w", **meta_full) as dst: dst.write(res, 1)
163
+ st.success("DSM Refinement Complete!")
164
+ st.download_button("πŸ“₯ Download Final DSM", buf.getvalue(), "refined_full_res.tif")
165
  else:
166
  st.info("Upload Assets to begin.")