Upload folder using huggingface_hub
Browse files- README.md +14 -0
- example_notebook.ipynb +0 -0
- example_on_training.ipynb +0 -0
- handcrafted_solution.py +513 -0
- params.json +23 -0
- script.py +132 -0
README.md
ADDED
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Empty solution example for the S23DR competition
|
2 |
+
|
3 |
+
This repo provides a minimalistic example of a valid, but empty submission to S23DR competition.
|
4 |
+
We recommend you take a look at [this example](https://huggingface.co/usm3d/handcrafted_baseline_submission),
|
5 |
+
which implements some primitive algorithms and provides useful I/O and visualization functions.
|
6 |
+
|
7 |
+
This example seeks to simply provide minimal code which succeeds at reading the dataset and producing a solution (in this case two vertices at the origin and edge of zero length connecting them).
|
8 |
+
|
9 |
+
`script.py` - is the main file which is run by the competition space. It should produce `submission.parquet` as the result of the run. Please see the additional comments in the `script.py` file.
|
10 |
+
|
11 |
+
---
|
12 |
+
license: apache-2.0
|
13 |
+
---
|
14 |
+
|
example_notebook.ipynb
ADDED
The diff for this file is too large to render.
See raw diff
|
|
example_on_training.ipynb
ADDED
The diff for this file is too large to render.
See raw diff
|
|
handcrafted_solution.py
ADDED
@@ -0,0 +1,513 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# Description: This file contains the handcrafted solution for the task of wireframe reconstruction
|
2 |
+
|
3 |
+
import io
|
4 |
+
from PIL import Image as PImage
|
5 |
+
import numpy as np
|
6 |
+
from collections import defaultdict
|
7 |
+
import cv2
|
8 |
+
from typing import Tuple, List
|
9 |
+
from scipy.spatial.distance import cdist
|
10 |
+
from scipy.optimize import minimize
|
11 |
+
|
12 |
+
|
13 |
+
|
14 |
+
def empty_solution():
|
15 |
+
'''Return a minimal valid solution, i.e. 2 vertices and 1 edge.'''
|
16 |
+
return np.zeros((2,3)), [(0, 1)]
|
17 |
+
|
18 |
+
def read_colmap_rec(colmap_data):
|
19 |
+
import pycolmap
|
20 |
+
import tempfile,zipfile
|
21 |
+
import io
|
22 |
+
with tempfile.TemporaryDirectory() as tmpdir:
|
23 |
+
with zipfile.ZipFile(io.BytesIO(colmap_data), "r") as zf:
|
24 |
+
zf.extractall(tmpdir) # unpacks cameras.txt, images.txt, etc. to tmpdir
|
25 |
+
# Now parse with pycolmap
|
26 |
+
rec = pycolmap.Reconstruction(tmpdir)
|
27 |
+
return rec
|
28 |
+
|
29 |
+
def convert_entry_to_human_readable(entry):
|
30 |
+
out = {}
|
31 |
+
for k, v in entry.items():
|
32 |
+
if 'colmap' in k:
|
33 |
+
out[k] = read_colmap_rec(v)
|
34 |
+
elif k in ['wf_vertices', 'wf_edges', 'K', 'R', 't']:
|
35 |
+
out[k] = np.array(v)
|
36 |
+
else:
|
37 |
+
out[k]=v
|
38 |
+
out['__key__'] = entry['order_id']
|
39 |
+
return out
|
40 |
+
|
41 |
+
|
42 |
+
def point_to_segment_dist(pt, seg_p1, seg_p2):
|
43 |
+
"""
|
44 |
+
Computes the Euclidean distance from pt to the line segment p1->p2.
|
45 |
+
pt, seg_p1, seg_p2: (x, y) as np.ndarray
|
46 |
+
"""
|
47 |
+
# If both endpoints are the same, just return distance to one of them
|
48 |
+
if np.allclose(seg_p1, seg_p2):
|
49 |
+
return np.linalg.norm(pt - seg_p1)
|
50 |
+
seg_vec = seg_p2 - seg_p1
|
51 |
+
pt_vec = pt - seg_p1
|
52 |
+
seg_len2 = seg_vec.dot(seg_vec)
|
53 |
+
t = max(0, min(1, pt_vec.dot(seg_vec)/seg_len2))
|
54 |
+
proj = seg_p1 + t*seg_vec
|
55 |
+
return np.linalg.norm(pt - proj)
|
56 |
+
|
57 |
+
|
58 |
+
def get_vertices_and_edges_from_segmentation(gest_seg_np, edge_th=25.0):
|
59 |
+
"""
|
60 |
+
Identify apex and eave-end vertices, then detect lines for eave/ridge/rake/valley.
|
61 |
+
For each connected component, we do a line fit with cv2.fitLine, then measure
|
62 |
+
segment endpoints more robustly. We then associate apex points that are within
|
63 |
+
'edge_th' of the line segment. We record those apex–apex connections for edges
|
64 |
+
if at least 2 apexes lie near the same component line.
|
65 |
+
"""
|
66 |
+
from hoho.color_mappings import gestalt_color_mapping # for apex, eave_end_point, etc.
|
67 |
+
|
68 |
+
#--------------------------------------------------------------------------------
|
69 |
+
# Step A: Collect apex and eave_end vertices
|
70 |
+
#--------------------------------------------------------------------------------
|
71 |
+
vertices = []
|
72 |
+
# Apex
|
73 |
+
apex_color = np.array(gestalt_color_mapping['apex'])
|
74 |
+
apex_mask = cv2.inRange(gest_seg_np, apex_color-0.5, apex_color+0.5)
|
75 |
+
if apex_mask.sum() > 0:
|
76 |
+
output = cv2.connectedComponentsWithStats(apex_mask, 8, cv2.CV_32S)
|
77 |
+
(numLabels, labels, stats, centroids) = output
|
78 |
+
stats, centroids = stats[1:], centroids[1:] # skip background
|
79 |
+
for i in range(numLabels-1):
|
80 |
+
vert = {"xy": centroids[i], "type": "apex"}
|
81 |
+
vertices.append(vert)
|
82 |
+
|
83 |
+
# Eave end
|
84 |
+
eave_end_color = np.array(gestalt_color_mapping['eave_end_point'])
|
85 |
+
eave_end_mask = cv2.inRange(gest_seg_np, eave_end_color-0.5, eave_end_color+0.5)
|
86 |
+
if eave_end_mask.sum() > 0:
|
87 |
+
output = cv2.connectedComponentsWithStats(eave_end_mask, 8, cv2.CV_32S)
|
88 |
+
(numLabels, labels, stats, centroids) = output
|
89 |
+
stats, centroids = stats[1:], centroids[1:]
|
90 |
+
for i in range(numLabels-1):
|
91 |
+
vert = {"xy": centroids[i], "type": "eave_end_point"}
|
92 |
+
vertices.append(vert)
|
93 |
+
|
94 |
+
# Consolidate apex points as array:
|
95 |
+
apex_pts = []
|
96 |
+
apex_idx_map = [] # keep track of index in 'vertices'
|
97 |
+
for idx, v in enumerate(vertices):
|
98 |
+
apex_pts.append(v['xy'])
|
99 |
+
apex_idx_map.append(idx)
|
100 |
+
apex_pts = np.array(apex_pts)
|
101 |
+
|
102 |
+
connections = []
|
103 |
+
edge_classes = ['eave', 'ridge', 'rake', 'valley']
|
104 |
+
for edge_class in edge_classes:
|
105 |
+
edge_color = np.array(gestalt_color_mapping[edge_class])
|
106 |
+
mask_raw = cv2.inRange(gest_seg_np, edge_color-0.5, edge_color+0.5)
|
107 |
+
|
108 |
+
# Possibly do morphological open/close to avoid merges or small holes
|
109 |
+
kernel = np.ones((5, 5), np.uint8) # smaller kernel to reduce over-merge
|
110 |
+
mask = cv2.morphologyEx(mask_raw, cv2.MORPH_CLOSE, kernel)
|
111 |
+
|
112 |
+
if mask.sum() == 0:
|
113 |
+
continue
|
114 |
+
|
115 |
+
# Connected components
|
116 |
+
output = cv2.connectedComponentsWithStats(mask, 8, cv2.CV_32S)
|
117 |
+
(numLabels, labels, stats, centroids) = output
|
118 |
+
# skip the background
|
119 |
+
stats, centroids = stats[1:], centroids[1:]
|
120 |
+
label_indices = range(1, numLabels)
|
121 |
+
|
122 |
+
# For each connected component, do a line fit
|
123 |
+
for lbl in label_indices:
|
124 |
+
ys, xs = np.where(labels == lbl)
|
125 |
+
if len(xs) < 2:
|
126 |
+
continue
|
127 |
+
# Fit a line using cv2.fitLine
|
128 |
+
pts_for_fit = np.column_stack([xs, ys]).astype(np.float32)
|
129 |
+
# (vx, vy, x0, y0) = direction + a point on the line
|
130 |
+
line_params = cv2.fitLine(pts_for_fit, distType=cv2.DIST_L2,
|
131 |
+
param=0, reps=0.01, aeps=0.01)
|
132 |
+
vx, vy, x0, y0 = line_params.ravel()
|
133 |
+
# We'll approximate endpoints by projecting (xs, ys) onto the line,
|
134 |
+
# then taking min and max in the 1D param along the line.
|
135 |
+
|
136 |
+
# param along the line = ( (x - x0)*vx + (y - y0)*vy )
|
137 |
+
proj = ( (xs - x0)*vx + (ys - y0)*vy )
|
138 |
+
proj_min, proj_max = proj.min(), proj.max()
|
139 |
+
p1 = np.array([x0 + proj_min*vx, y0 + proj_min*vy])
|
140 |
+
p2 = np.array([x0 + proj_max*vx, y0 + proj_max*vy])
|
141 |
+
|
142 |
+
#--------------------------------------------------------------------------------
|
143 |
+
# Step C: If apex points are within 'edge_th' of segment, they are connected
|
144 |
+
#--------------------------------------------------------------------------------
|
145 |
+
if len(apex_pts) < 2:
|
146 |
+
continue
|
147 |
+
|
148 |
+
# Distance from each apex to the line segment
|
149 |
+
dists = np.array([
|
150 |
+
point_to_segment_dist(apex_pts[i], p1, p2)
|
151 |
+
for i in range(len(apex_pts))
|
152 |
+
])
|
153 |
+
|
154 |
+
# Indices of apex points that are near
|
155 |
+
near_mask = (dists <= edge_th)
|
156 |
+
near_indices = np.where(near_mask)[0]
|
157 |
+
if len(near_indices) < 2:
|
158 |
+
continue
|
159 |
+
|
160 |
+
# Connect each pair among these near apex points
|
161 |
+
for i in range(len(near_indices)):
|
162 |
+
for j in range(i+1, len(near_indices)):
|
163 |
+
a_idx = near_indices[i]
|
164 |
+
b_idx = near_indices[j]
|
165 |
+
# 'a_idx' and 'b_idx' are indices in apex_pts / apex_idx_map
|
166 |
+
vA = apex_idx_map[a_idx]
|
167 |
+
vB = apex_idx_map[b_idx]
|
168 |
+
# Store the connection using sorted indexing
|
169 |
+
conn = tuple(sorted((vA, vB)))
|
170 |
+
connections.append(conn)
|
171 |
+
|
172 |
+
return vertices, connections
|
173 |
+
|
174 |
+
|
175 |
+
def get_uv_depth(vertices, depth_fitted, sparse_depth, search_radius=10):
|
176 |
+
"""
|
177 |
+
For each vertex, returns a 2D array of (u,v) and a matching 1D array of depths.
|
178 |
+
|
179 |
+
We attempt to use the sparse_depth if available in a local neighborhood:
|
180 |
+
1. For each vertex coordinate (x, y), define a local window in sparse_depth
|
181 |
+
of size (2*search_radius + 1).
|
182 |
+
2. Collect all valid (nonzero) values in that window.
|
183 |
+
3. If any exist, we take the median (robust) as the vertex depth.
|
184 |
+
4. Otherwise, we use depth_fitted[y, x].
|
185 |
+
|
186 |
+
Parameters
|
187 |
+
----------
|
188 |
+
vertices : List[dict]
|
189 |
+
Each dict must have "xy" at least, e.g. {"xy": (x, y), ...}
|
190 |
+
depth_fitted : np.ndarray
|
191 |
+
A 2D array (H, W), the dense (or corrected) depth for fallback.
|
192 |
+
sparse_depth : np.ndarray
|
193 |
+
A 2D array (H, W), mostly zeros except where accurate data is available.
|
194 |
+
search_radius : int
|
195 |
+
Pixel radius around the vertex in which to look for sparse depth values.
|
196 |
+
|
197 |
+
Returns
|
198 |
+
-------
|
199 |
+
uv : np.ndarray of shape (N, 2)
|
200 |
+
2D float coordinates of each vertex (x, y).
|
201 |
+
vertex_depth : np.ndarray of shape (N,)
|
202 |
+
Depth value chosen for each vertex.
|
203 |
+
"""
|
204 |
+
|
205 |
+
# Collect each vertex's (x, y)
|
206 |
+
uv = np.array([v['xy'] for v in vertices], dtype=np.float32)
|
207 |
+
# Convert to integer pixel coordinates (round or floor)
|
208 |
+
uv_int = np.round(uv).astype(np.int32)
|
209 |
+
|
210 |
+
H, W = depth_fitted.shape[:2]
|
211 |
+
# Clip coordinates to stay within image bounds
|
212 |
+
uv_int[:, 0] = np.clip(uv_int[:, 0], 0, W-1)
|
213 |
+
uv_int[:, 1] = np.clip(uv_int[:, 1], 0, H-1)
|
214 |
+
|
215 |
+
# Prepare output array of depths
|
216 |
+
vertex_depth = np.zeros(len(vertices), dtype=np.float32)
|
217 |
+
|
218 |
+
for i, (x_i, y_i) in enumerate(uv_int):
|
219 |
+
# Local region in [x_i - search_radius, x_i + search_radius]
|
220 |
+
x0 = max(0, x_i - search_radius)
|
221 |
+
x1 = min(W, x_i + search_radius + 1)
|
222 |
+
y0 = max(0, y_i - search_radius)
|
223 |
+
y1 = min(H, y_i + search_radius + 1)
|
224 |
+
|
225 |
+
region = sparse_depth[y0:y1, x0:x1]
|
226 |
+
valid_vals = region[region > 0]
|
227 |
+
if len(valid_vals) > 0:
|
228 |
+
# Use median of valid sparse depth
|
229 |
+
vertex_depth[i] = np.median(valid_vals)
|
230 |
+
else:
|
231 |
+
# Fallback to depth_fitted at this pixel
|
232 |
+
vertex_depth[i] = depth_fitted[y_i, x_i]
|
233 |
+
|
234 |
+
return uv, vertex_depth
|
235 |
+
|
236 |
+
def merge_vertices_3d(vert_edge_per_image, th=0.5):
|
237 |
+
'''Merge vertices that are close to each other in 3D space and are of same types'''
|
238 |
+
all_3d_vertices = []
|
239 |
+
connections_3d = []
|
240 |
+
all_indexes = []
|
241 |
+
cur_start = 0
|
242 |
+
types = []
|
243 |
+
for cimg_idx, (vertices, connections, vertices_3d) in vert_edge_per_image.items():
|
244 |
+
types += [int(v['type']=='apex') for v in vertices]
|
245 |
+
all_3d_vertices.append(vertices_3d)
|
246 |
+
connections_3d+=[(x+cur_start,y+cur_start) for (x,y) in connections]
|
247 |
+
cur_start+=len(vertices_3d)
|
248 |
+
all_3d_vertices = np.concatenate(all_3d_vertices, axis=0)
|
249 |
+
#print (connections_3d)
|
250 |
+
distmat = cdist(all_3d_vertices, all_3d_vertices)
|
251 |
+
types = np.array(types).reshape(-1,1)
|
252 |
+
same_types = cdist(types, types)
|
253 |
+
mask_to_merge = (distmat <= th) & (same_types==0)
|
254 |
+
new_vertices = []
|
255 |
+
new_connections = []
|
256 |
+
to_merge = sorted(list(set([tuple(a.nonzero()[0].tolist()) for a in mask_to_merge])))
|
257 |
+
to_merge_final = defaultdict(list)
|
258 |
+
for i in range(len(all_3d_vertices)):
|
259 |
+
for j in to_merge:
|
260 |
+
if i in j:
|
261 |
+
to_merge_final[i]+=j
|
262 |
+
for k, v in to_merge_final.items():
|
263 |
+
to_merge_final[k] = list(set(v))
|
264 |
+
already_there = set()
|
265 |
+
merged = []
|
266 |
+
for k, v in to_merge_final.items():
|
267 |
+
if k in already_there:
|
268 |
+
continue
|
269 |
+
merged.append(v)
|
270 |
+
for vv in v:
|
271 |
+
already_there.add(vv)
|
272 |
+
old_idx_to_new = {}
|
273 |
+
count=0
|
274 |
+
for idxs in merged:
|
275 |
+
new_vertices.append(all_3d_vertices[idxs].mean(axis=0))
|
276 |
+
for idx in idxs:
|
277 |
+
old_idx_to_new[idx] = count
|
278 |
+
count +=1
|
279 |
+
#print (connections_3d)
|
280 |
+
new_vertices=np.array(new_vertices)
|
281 |
+
#print (connections_3d)
|
282 |
+
for conn in connections_3d:
|
283 |
+
new_con = sorted((old_idx_to_new[conn[0]], old_idx_to_new[conn[1]]))
|
284 |
+
if new_con[0] == new_con[1]:
|
285 |
+
continue
|
286 |
+
if new_con not in new_connections:
|
287 |
+
new_connections.append(new_con)
|
288 |
+
#print (f'{len(new_vertices)} left after merging {len(all_3d_vertices)} with {th=}')
|
289 |
+
return new_vertices, new_connections
|
290 |
+
|
291 |
+
|
292 |
+
def prune_not_connected(all_3d_vertices, connections_3d, keep_largest=True):
|
293 |
+
"""
|
294 |
+
Prune vertices not connected to anything. If keep_largest=True, also
|
295 |
+
keep only the largest connected component in the graph.
|
296 |
+
"""
|
297 |
+
if len(all_3d_vertices) == 0:
|
298 |
+
return np.array([]), []
|
299 |
+
|
300 |
+
# adjacency
|
301 |
+
adj = defaultdict(set)
|
302 |
+
for (i, j) in connections_3d:
|
303 |
+
adj[i].add(j)
|
304 |
+
adj[j].add(i)
|
305 |
+
|
306 |
+
# keep only vertices that appear in at least one edge
|
307 |
+
used_idxs = set()
|
308 |
+
for (i, j) in connections_3d:
|
309 |
+
used_idxs.add(i)
|
310 |
+
used_idxs.add(j)
|
311 |
+
|
312 |
+
if not used_idxs:
|
313 |
+
return np.empty((0,3)), []
|
314 |
+
|
315 |
+
# If we only want to remove truly isolated points, but keep multiple subgraphs:
|
316 |
+
if not keep_largest:
|
317 |
+
new_map = {}
|
318 |
+
used_list = sorted(list(used_idxs))
|
319 |
+
for new_id, old_id in enumerate(used_list):
|
320 |
+
new_map[old_id] = new_id
|
321 |
+
new_vertices = np.array([all_3d_vertices[old_id] for old_id in used_list])
|
322 |
+
new_conns = []
|
323 |
+
for (i, j) in connections_3d:
|
324 |
+
if i in used_idxs and j in used_idxs:
|
325 |
+
new_conns.append((new_map[i], new_map[j]))
|
326 |
+
return new_vertices, new_conns
|
327 |
+
|
328 |
+
# Otherwise find the largest connected component:
|
329 |
+
visited = set()
|
330 |
+
def bfs(start):
|
331 |
+
queue = [start]
|
332 |
+
comp = []
|
333 |
+
visited.add(start)
|
334 |
+
while queue:
|
335 |
+
cur = queue.pop()
|
336 |
+
comp.append(cur)
|
337 |
+
for neigh in adj[cur]:
|
338 |
+
if neigh not in visited:
|
339 |
+
visited.add(neigh)
|
340 |
+
queue.append(neigh)
|
341 |
+
return comp
|
342 |
+
|
343 |
+
# Collect all subgraphs
|
344 |
+
comps = []
|
345 |
+
for idx in used_idxs:
|
346 |
+
if idx not in visited:
|
347 |
+
c = bfs(idx)
|
348 |
+
comps.append(c)
|
349 |
+
|
350 |
+
# pick largest
|
351 |
+
comps.sort(key=lambda c: len(c), reverse=True)
|
352 |
+
largest = comps[0] if len(comps)>0 else []
|
353 |
+
|
354 |
+
# Remap
|
355 |
+
new_map = {}
|
356 |
+
for new_id, old_id in enumerate(largest):
|
357 |
+
new_map[old_id] = new_id
|
358 |
+
|
359 |
+
new_vertices = np.array([all_3d_vertices[old_id] for old_id in largest])
|
360 |
+
new_conns = []
|
361 |
+
for (i, j) in connections_3d:
|
362 |
+
if i in largest and j in largest:
|
363 |
+
new_conns.append((new_map[i], new_map[j]))
|
364 |
+
|
365 |
+
# remove duplicates
|
366 |
+
new_conns = list(set([tuple(sorted(c)) for c in new_conns]))
|
367 |
+
return new_vertices, new_conns
|
368 |
+
|
369 |
+
|
370 |
+
|
371 |
+
def get_sparse_depth(colmap_rec, img_id, K, R, t, depth):
|
372 |
+
H, W = depth.shape
|
373 |
+
xyz = []
|
374 |
+
rgb = []
|
375 |
+
found = False
|
376 |
+
for img_id_c, col_img in colmap_rec.images.items():
|
377 |
+
if col_img.name == img_id:
|
378 |
+
found = True
|
379 |
+
break
|
380 |
+
if not found:
|
381 |
+
return np.zeros((H, W), dtype=np.float32), False
|
382 |
+
mat4x4 = np.eye(4)
|
383 |
+
mat4x4[:3 ] = col_img.cam_from_world.matrix()
|
384 |
+
for pid,p in colmap_rec.points3D.items():
|
385 |
+
if col_img.has_point3D(pid):
|
386 |
+
xyz.append(p.xyz)
|
387 |
+
rgb.append(p.color)
|
388 |
+
xyz = np.array(xyz)
|
389 |
+
rgb = np.array(rgb)
|
390 |
+
xyz_projected = cv2.transform(cv2.convertPointsToHomogeneous(xyz), mat4x4)
|
391 |
+
xyz_projected = cv2.convertPointsFromHomogeneous(xyz_projected).reshape(-1, 3)
|
392 |
+
uv, _ = cv2.projectPoints(xyz_projected, np.zeros(3), np.zeros(3), np.array(K), np.zeros(4))
|
393 |
+
uv = uv.squeeze()
|
394 |
+
u, v = uv[:, 0].astype(np.int32), uv[:, 1].astype(np.int32)
|
395 |
+
mask = (u >= 0) & (u < W) & (v >= 0) & (v < H)
|
396 |
+
u, v = u[mask], v[mask]
|
397 |
+
xyz_projected, rgb = xyz_projected[mask], rgb[mask]
|
398 |
+
depth = np.zeros((H, W), dtype=np.float32)
|
399 |
+
depth[v, u] = xyz_projected[:, 2]
|
400 |
+
return depth, True
|
401 |
+
|
402 |
+
|
403 |
+
def fit_scale_robust_median(depth, sparse_depth):
|
404 |
+
"""
|
405 |
+
Fits the model sparse_depth ~ k * depth + b by minimizing the median
|
406 |
+
of absolute residuals, i.e. median( |sparse_depth - (k*depth + b)| ).
|
407 |
+
|
408 |
+
Parameters
|
409 |
+
----------
|
410 |
+
depth : np.ndarray
|
411 |
+
Array of depth estimates (same shape as sparse_depth).
|
412 |
+
sparse_depth : np.ndarray
|
413 |
+
Sparse array with precise depth at certain locations
|
414 |
+
(0 where data is unavailable).
|
415 |
+
|
416 |
+
Returns
|
417 |
+
-------
|
418 |
+
k : float
|
419 |
+
The slope of the robust best-fit affine transform.
|
420 |
+
b : float
|
421 |
+
The intercept of the robust best-fit affine transform.
|
422 |
+
depth_fitted : np.ndarray
|
423 |
+
The depth array adjusted by the affine fit: k*depth + b.
|
424 |
+
"""
|
425 |
+
|
426 |
+
# 1. Create mask of valid (nonzero) locations in sparse_depth
|
427 |
+
mask = (sparse_depth != 0)
|
428 |
+
X = depth[mask]
|
429 |
+
Y = sparse_depth[mask]
|
430 |
+
|
431 |
+
# 2. Define the objective: median of absolute residuals
|
432 |
+
def median_abs_resid(params, xvals, yvals):
|
433 |
+
k, b = params
|
434 |
+
return np.median(np.abs(yvals - (k*xvals)))
|
435 |
+
|
436 |
+
# 3. Get an initial guess from a standard least-squares fit
|
437 |
+
# (this helps the optimizer start in a reasonable region)
|
438 |
+
k_init, b_init = np.polyfit(X, Y, deg=1)
|
439 |
+
|
440 |
+
# 4. Optimize using a derivative-free method (Nelder-Mead)
|
441 |
+
res = minimize(
|
442 |
+
fun=median_abs_resid,
|
443 |
+
x0=[k_init, b_init],
|
444 |
+
args=(X, Y),
|
445 |
+
method='Nelder-Mead'
|
446 |
+
)
|
447 |
+
|
448 |
+
k_robust, b_robust = res.x
|
449 |
+
|
450 |
+
# 5. Construct the fitted depth array
|
451 |
+
depth_fitted = k_robust * depth #+ b_robust
|
452 |
+
|
453 |
+
return k_robust, depth_fitted
|
454 |
+
|
455 |
+
|
456 |
+
|
457 |
+
|
458 |
+
def predict(entry, visualize=False) -> Tuple[np.ndarray, List[int]]:
|
459 |
+
good_entry = convert_entry_to_human_readable(entry)
|
460 |
+
vert_edge_per_image = {}
|
461 |
+
for i, (gest, depth, K, R, t, img_id) in enumerate(zip(good_entry['gestalt'],
|
462 |
+
good_entry['depth'],
|
463 |
+
good_entry['K'],
|
464 |
+
good_entry['R'],
|
465 |
+
good_entry['t'],
|
466 |
+
good_entry['image_ids']
|
467 |
+
)):
|
468 |
+
colmap_rec = good_entry['colmap_binary']
|
469 |
+
K = np.array(K)
|
470 |
+
R = np.array(R)
|
471 |
+
t = np.array(t)
|
472 |
+
gest_seg = gest.resize(depth.size)
|
473 |
+
gest_seg_np = np.array(gest_seg).astype(np.uint8)
|
474 |
+
# Metric3D
|
475 |
+
depth_np = np.array(depth) / 1000.
|
476 |
+
depth_sparse, found = get_sparse_depth(colmap_rec, img_id, K, R, t, depth_np)
|
477 |
+
if not found:
|
478 |
+
print (f'No sparse depth found for image {i}')
|
479 |
+
vert_edge_per_image[i] = np.empty((0, 2)), [], np.empty((0, 3))
|
480 |
+
continue
|
481 |
+
k, depth_fitted = fit_scale_robust_median(depth_np, depth_sparse)#fit_affine_robust_median(depth_np, depth_sparse)
|
482 |
+
print (k)
|
483 |
+
vertices, connections = get_vertices_and_edges_from_segmentation(gest_seg_np, edge_th = 50.)
|
484 |
+
if (len(vertices) < 2) or (len(connections) < 1):
|
485 |
+
print (f'Not enough vertices or connections in image {i}')
|
486 |
+
vert_edge_per_image[i] = np.empty((0, 2)), [], np.empty((0, 3))
|
487 |
+
continue
|
488 |
+
|
489 |
+
uv, depth_vert = get_uv_depth(vertices, depth_fitted, depth_sparse, 50)
|
490 |
+
# Normalize the uv to the camera intrinsics
|
491 |
+
X = (uv[:, 0] - K[0, 2]) / K[0, 0] * depth_vert
|
492 |
+
Y = (uv[:, 1] - K[1, 2]) / K[1, 1] * depth_vert
|
493 |
+
Z = depth_vert
|
494 |
+
vertices_3d_local = np.column_stack([X, Y, Z])
|
495 |
+
world_to_cam = np.eye(4)
|
496 |
+
world_to_cam[:3, :3] = R
|
497 |
+
world_to_cam[:3, 3] = t.reshape(-1)
|
498 |
+
cam_to_world = np.linalg.inv(world_to_cam)
|
499 |
+
vertices_3d = cv2.transform(cv2.convertPointsToHomogeneous(vertices_3d_local), cam_to_world)
|
500 |
+
vertices_3d = cv2.convertPointsFromHomogeneous(vertices_3d).reshape(-1, 3)
|
501 |
+
vert_edge_per_image[i] = vertices, connections, vertices_3d
|
502 |
+
all_3d_vertices, connections_3d = merge_vertices_3d(vert_edge_per_image, 0.25)
|
503 |
+
all_3d_vertices_clean, connections_3d_clean = prune_not_connected(all_3d_vertices, connections_3d, keep_largest=False)
|
504 |
+
if (len(all_3d_vertices_clean) < 2) or len(connections_3d_clean) < 1:
|
505 |
+
print (f'Not enough vertices or connections in the 3D vertices')
|
506 |
+
return empty_solution()
|
507 |
+
if visualize:
|
508 |
+
from hoho.viz3d import plot_estimate_and_gt
|
509 |
+
plot_estimate_and_gt( all_3d_vertices_clean,
|
510 |
+
connections_3d_clean,
|
511 |
+
good_entry['wf_vertices'],
|
512 |
+
good_entry['wf_edges'])
|
513 |
+
return all_3d_vertices_clean, connections_3d_clean
|
params.json
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"competition_id": "usm3d/S23DR2025",
|
3 |
+
"competition_type": "script",
|
4 |
+
"metric": "custom",
|
5 |
+
"token": "hf_******",
|
6 |
+
"team_id": "xxxxxxxxx_your_team_name_xxxxxxxxxx",
|
7 |
+
"submission_id": "xxxxxxxxx_your_sub_id_xxxxxxxxxx",
|
8 |
+
"submission_id_col": "order_id",
|
9 |
+
"submission_cols": [
|
10 |
+
"order_id",
|
11 |
+
"wf_vertices",
|
12 |
+
"wf_edges",
|
13 |
+
"wf_classifications"
|
14 |
+
],
|
15 |
+
"submission_rows": 267,
|
16 |
+
"output_path": "/tmp/model",
|
17 |
+
"submission_repo": "<your submission repo>",
|
18 |
+
"time_limit": 7200,
|
19 |
+
"dataset": "usm3d/hoho25k_test_x",
|
20 |
+
"submission_filenames": [
|
21 |
+
"submission.parquet"
|
22 |
+
]
|
23 |
+
}
|
script.py
ADDED
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
### This is example of the script that will be run in the test environment.
|
2 |
+
|
3 |
+
### You can change the rest of the code to define and test your solution.
|
4 |
+
### However, you should not change the signature of the provided function.
|
5 |
+
### The script saves "submission.parquet" file in the current directory.
|
6 |
+
### You can use any additional files and subdirectories to organize your code.
|
7 |
+
|
8 |
+
from pathlib import Path
|
9 |
+
from tqdm import tqdm
|
10 |
+
import pandas as pd
|
11 |
+
import numpy as np
|
12 |
+
from datasets import load_dataset
|
13 |
+
from typing import Dict
|
14 |
+
|
15 |
+
def empty_solution(sample):
|
16 |
+
'''Return a minimal valid solution, i.e. 2 vertices and 1 edge.'''
|
17 |
+
return np.zeros((2,3)), [(0, 1)]
|
18 |
+
from handcrafted_solution import predict
|
19 |
+
|
20 |
+
class Sample(Dict):
|
21 |
+
def pick_repr_data(self, x):
|
22 |
+
if hasattr(x, 'shape'):
|
23 |
+
return x.shape
|
24 |
+
if isinstance(x, (str, float, int)):
|
25 |
+
return x
|
26 |
+
if isinstance(x, list):
|
27 |
+
return [type(x[0])] if len(x) > 0 else []
|
28 |
+
return type(x)
|
29 |
+
|
30 |
+
def __repr__(self):
|
31 |
+
# return str({k: v.shape if hasattr(v, 'shape') else [type(v[0])] if isinstance(v, list) else type(v) for k,v in self.items()})
|
32 |
+
return str({k: self.pick_repr_data(v) for k,v in self.items()})
|
33 |
+
|
34 |
+
import json
|
35 |
+
if __name__ == "__main__":
|
36 |
+
print ("------------ Loading dataset------------ ")
|
37 |
+
param_path = Path('params.json')
|
38 |
+
print(param_path)
|
39 |
+
with param_path.open() as f:
|
40 |
+
params = json.load(f)
|
41 |
+
print(params)
|
42 |
+
import os
|
43 |
+
|
44 |
+
print('pwd:')
|
45 |
+
os.system('pwd')
|
46 |
+
print(os.system('ls -lahtr'))
|
47 |
+
print('/tmp/data/')
|
48 |
+
print(os.system('ls -lahtr /tmp/data/'))
|
49 |
+
print('/tmp/data/data')
|
50 |
+
print(os.system('ls -lahtrR /tmp/data/data'))
|
51 |
+
|
52 |
+
|
53 |
+
data_path_test_server = Path('/tmp/data')
|
54 |
+
data_path_local = Path().home() / '.cache/huggingface/datasets/usm3d___hoho25k_test_x/'
|
55 |
+
|
56 |
+
if data_path_test_server.exists():
|
57 |
+
# data_path = data_path_test_server
|
58 |
+
TEST_ENV = True
|
59 |
+
else:
|
60 |
+
# data_path = data_path_local
|
61 |
+
TEST_ENV = False
|
62 |
+
from huggingface_hub import snapshot_download
|
63 |
+
_ = snapshot_download(
|
64 |
+
repo_id=params['dataset'],
|
65 |
+
local_dir="/tmp/data",
|
66 |
+
repo_type="dataset",
|
67 |
+
)
|
68 |
+
data_path = data_path_test_server
|
69 |
+
|
70 |
+
|
71 |
+
print(data_path)
|
72 |
+
|
73 |
+
# dataset = load_dataset(params['dataset'], trust_remote_code=True, use_auth_token=params['token'])
|
74 |
+
# data_files = {
|
75 |
+
# "validation": [str(p) for p in [*data_path.rglob('*validation*.arrow')]+[*data_path.rglob('*public*/**/*.tar')]],
|
76 |
+
# "test": [str(p) for p in [*data_path.rglob('*test*.arrow')]+[*data_path.rglob('*private*/**/*.tar')]],
|
77 |
+
# }
|
78 |
+
data_files = {
|
79 |
+
"validation": [str(p) for p in data_path.rglob('*public*/**/*.tar')],
|
80 |
+
"test": [str(p) for p in data_path.rglob('*private*/**/*.tar')],
|
81 |
+
}
|
82 |
+
print(data_files)
|
83 |
+
dataset = load_dataset(
|
84 |
+
str(data_path / 'hoho25k_test_x.py'),
|
85 |
+
data_files=data_files,
|
86 |
+
trust_remote_code=True,
|
87 |
+
writer_batch_size=100
|
88 |
+
)
|
89 |
+
|
90 |
+
# if TEST_ENV:
|
91 |
+
# dataset = load_dataset(
|
92 |
+
# "webdataset",
|
93 |
+
# data_files=data_files,
|
94 |
+
# trust_remote_code=True,
|
95 |
+
# # streaming=True
|
96 |
+
# )
|
97 |
+
print('load with webdataset')
|
98 |
+
# else:
|
99 |
+
|
100 |
+
# dataset = load_dataset(
|
101 |
+
# "arrow",
|
102 |
+
# data_files=data_files,
|
103 |
+
# trust_remote_code=True,
|
104 |
+
# # streaming=True
|
105 |
+
# )
|
106 |
+
# print('load with arrow')
|
107 |
+
|
108 |
+
|
109 |
+
print(dataset, flush=True)
|
110 |
+
# dataset = load_dataset('webdataset', data_files={)
|
111 |
+
|
112 |
+
print('------------ Now you can do your solution ---------------')
|
113 |
+
solution = []
|
114 |
+
for subset_name in dataset:
|
115 |
+
for i, sample in enumerate(tqdm(dataset[subset_name])):
|
116 |
+
# replace this with your solution
|
117 |
+
print(Sample(sample), flush=True)
|
118 |
+
print('------')
|
119 |
+
try:
|
120 |
+
pred_vertices, pred_edges = predict(sample, visualize=False)
|
121 |
+
except:
|
122 |
+
pred_vertices, pred_edges = empty_solution(sample)
|
123 |
+
solution.append({
|
124 |
+
'order_id': sample['order_id'],
|
125 |
+
'wf_vertices': pred_vertices.tolist(),
|
126 |
+
'wf_edges': pred_edges
|
127 |
+
})
|
128 |
+
|
129 |
+
print('------------ Saving results ---------------')
|
130 |
+
sub = pd.DataFrame(solution, columns=["order_id", "wf_vertices", "wf_edges"])
|
131 |
+
sub.to_parquet("submission.parquet")
|
132 |
+
print("------------ Done ------------ ")
|