Raffael-Kultyshev commited on
Commit
eb39aa1
·
1 Parent(s): 7b3b386

Full app with exact reference structure: build_interface() + demo at module level

Browse files
Files changed (1) hide show
  1. app.py +351 -9
app.py CHANGED
@@ -1,16 +1,358 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  """
2
- Minimal test version
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
  """
4
- import gradio as gr
5
- from pathlib import Path
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6
 
7
- DATA_DIR = Path(__file__).parent / "data"
8
- video_path = DATA_DIR / "video.mp4"
 
9
 
10
- with gr.Blocks() as demo:
11
- gr.Markdown("# Test")
12
- gr.Video(value=str(video_path) if video_path.exists() else None)
13
 
14
  if __name__ == "__main__":
15
- demo.launch()
16
 
 
 
1
+ import json
2
+ import html
3
+ from pathlib import Path
4
+ from typing import Dict, List
5
+ from functools import lru_cache
6
+
7
+ import gradio as gr
8
+ import plotly.graph_objects as go
9
+ import plotly.io as pio
10
+
11
+ METRIC_LABELS = {
12
+ "x_cm": "X (cm)",
13
+ "y_cm": "Y (cm)",
14
+ "z_cm": "Z (cm)",
15
+ "yaw_deg": "Yaw (°)",
16
+ "pitch_deg": "Pitch (°)",
17
+ "roll_deg": "Roll (°)",
18
+ }
19
+
20
+ PLOT_GRID = [
21
+ ["x_cm", "y_cm", "z_cm"],
22
+ ["yaw_deg", "pitch_deg", "roll_deg"],
23
+ ]
24
+
25
+ PLOT_ORDER = [metric for row in PLOT_GRID for metric in row]
26
+
27
+ CUSTOM_CSS = """
28
+ :root, .gradio-container, body {
29
+ background-color: #050a18 !important;
30
+ color: #f8fafc !important;
31
+ font-family: 'Inter', 'Segoe UI', system-ui, sans-serif;
32
+ }
33
+ .side-panel {
34
+ background: #0f172a;
35
+ padding: 20px;
36
+ border-radius: 18px;
37
+ border: 1px solid #1f2b47;
38
+ min-height: 100%;
39
+ }
40
+ .stats-card ul {
41
+ list-style: none;
42
+ padding: 0;
43
+ margin: 0;
44
+ font-size: 0.92rem;
45
+ }
46
+ .stats-card li {
47
+ margin-bottom: 10px;
48
+ color: #e2e8f0;
49
+ }
50
+ .stats-card span {
51
+ display: inline-block;
52
+ margin-right: 6px;
53
+ color: #7dd3fc;
54
+ }
55
+ .main-panel {
56
+ padding-top: 8px;
57
+ }
58
+ .instruction-card {
59
+ background: #0f172a;
60
+ padding: 18px 20px;
61
+ border-radius: 18px;
62
+ border: 1px solid #1f2b47;
63
+ }
64
+ .instruction-label {
65
+ font-size: 0.75rem;
66
+ letter-spacing: 0.12em;
67
+ text-transform: uppercase;
68
+ color: #94a3b8;
69
+ margin-bottom: 10px;
70
+ }
71
+ .instruction-text {
72
+ font-size: 1.1rem;
73
+ line-height: 1.5;
74
+ }
75
+ .video-card {
76
+ background: #0f172a;
77
+ border: 1px solid #1f2b47;
78
+ border-radius: 18px;
79
+ padding: 18px 20px;
80
+ margin-top: 18px;
81
+ }
82
+ .video-title {
83
+ font-size: 0.78rem;
84
+ text-transform: uppercase;
85
+ letter-spacing: 0.18em;
86
+ color: #94a3b8;
87
+ margin-bottom: 8px;
88
+ }
89
+ .video-panel video {
90
+ border-radius: 12px;
91
+ border: 1px solid #1f2b47;
92
+ background: #030712;
93
+ }
94
+ .download-button button {
95
+ border-radius: 999px;
96
+ border: 1px solid #334155;
97
+ background: #1e293b;
98
+ color: #f8fafc;
99
+ font-size: 0.85rem;
100
+ padding: 8px 24px;
101
+ }
102
+ .download-button button:hover {
103
+ border-color: #67e8f9;
104
+ color: #67e8f9;
105
+ }
106
+ .plots-wrap {
107
+ margin-top: 18px;
108
+ }
109
+ .plots-wrap .gr-row {
110
+ gap: 16px;
111
+ }
112
+ .plot-html {
113
+ background: #111a2c;
114
+ border-radius: 12px;
115
+ padding: 10px;
116
+ border: 1px solid #1f2b47;
117
+ min-height: 320px;
118
+ }
119
+ .plot-html iframe {
120
+ width: 100%;
121
+ height: 300px;
122
+ border: none;
123
+ }
124
  """
125
+
126
+
127
+ def get_data_dir():
128
+ """Get data directory path."""
129
+ try:
130
+ return Path(__file__).parent / "data"
131
+ except:
132
+ return Path("data")
133
+
134
+
135
+ @lru_cache(maxsize=1)
136
+ def load_data():
137
+ """Load all data files."""
138
+ data_dir = get_data_dir()
139
+ metadata_path = data_dir / "metadata.json"
140
+ end_effector_path = data_dir / "end_effector.json"
141
+ hands_2d_path = data_dir / "hands_2d.json"
142
+
143
+ metadata = {}
144
+ end_effector = {}
145
+ hands_2d = {}
146
+
147
+ if metadata_path.exists():
148
+ with open(metadata_path, 'r') as f:
149
+ metadata = json.load(f)
150
+
151
+ if end_effector_path.exists():
152
+ with open(end_effector_path, 'r') as f:
153
+ end_effector = json.load(f)
154
+
155
+ if hands_2d_path.exists():
156
+ with open(hands_2d_path, 'r') as f:
157
+ hands_2d = json.load(f)
158
+
159
+ return metadata, end_effector, hands_2d
160
+
161
+
162
+ def build_state_dataframe(metadata: dict, end_effector: dict, hand: str = "left"):
163
+ """Build state dataframe from JSON data."""
164
+ fps = metadata.get('fps', 60)
165
+
166
+ try:
167
+ frame_keys = sorted([int(k) for k in end_effector.keys() if str(k).isdigit()])
168
+ except:
169
+ frame_keys = []
170
+
171
+ timestamps = []
172
+ state_data = {
173
+ 'wrist_x_cm': [],
174
+ 'wrist_y_cm': [],
175
+ 'wrist_z_cm': [],
176
+ 'wrist_yaw_deg': [],
177
+ 'wrist_pitch_deg': [],
178
+ 'wrist_roll_deg': [],
179
+ }
180
+
181
+ for frame_idx in frame_keys:
182
+ frame_key = str(frame_idx)
183
+ t = frame_idx / fps
184
+ timestamps.append(t)
185
+
186
+ ee_data = end_effector.get(frame_key, {}) or {}
187
+ hand_data = ee_data.get(hand + "_hand")
188
+
189
+ if hand_data and isinstance(hand_data, dict):
190
+ pose = hand_data.get('pose_6dof')
191
+ if pose and len(pose) >= 6:
192
+ state_data['wrist_x_cm'].append(pose[0] * 100) # m to cm
193
+ state_data['wrist_y_cm'].append(pose[1] * 100)
194
+ state_data['wrist_z_cm'].append(pose[2] * 100)
195
+ state_data['wrist_roll_deg'].append(pose[3] * 57.3) # rad to deg
196
+ state_data['wrist_pitch_deg'].append(pose[4] * 57.3)
197
+ state_data['wrist_yaw_deg'].append(pose[5] * 57.3)
198
+ else:
199
+ for k in state_data:
200
+ state_data[k].append(None)
201
+ else:
202
+ for k in state_data:
203
+ state_data[k].append(None)
204
+
205
+ return timestamps, state_data
206
+
207
+
208
+ def build_plot_fig(timestamps: List[float], state_data: Dict, metric: str) -> go.Figure:
209
+ """Build Plotly figure for a metric."""
210
+ col_name = f"wrist_{metric}"
211
+ if col_name not in state_data:
212
+ return go.Figure()
213
+
214
+ fig = go.Figure()
215
+ fig.add_trace(
216
+ go.Scatter(
217
+ x=timestamps,
218
+ y=state_data[col_name],
219
+ mode="lines",
220
+ name="Wrist",
221
+ )
222
+ )
223
+ fig.update_layout(
224
+ margin=dict(l=20, r=20, t=30, b=20),
225
+ height=250,
226
+ template="plotly_dark",
227
+ legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
228
+ xaxis_title="Time (s)",
229
+ yaxis_title=METRIC_LABELS[metric],
230
+ )
231
+ fig.update_xaxes(showgrid=True, gridwidth=0.5, gridcolor="rgba(255,255,255,0.1)")
232
+ fig.update_yaxes(showgrid=True, gridwidth=0.5, gridcolor="rgba(255,255,255,0.1)")
233
+ return fig
234
+
235
+
236
+ def build_plot_html(timestamps: List[float], state_data: Dict, metric: str) -> str:
237
+ """Build Plotly HTML for a metric."""
238
+ fig = build_plot_fig(timestamps, state_data, metric)
239
+ return pio.to_html(fig, include_plotlyjs="cdn", full_html=False)
240
+
241
+
242
+ def format_instruction_html(text: str) -> str:
243
+ safe_text = html.escape(text)
244
+ return (
245
+ '<div class="instruction-card">'
246
+ '<p class="instruction-label">Language Instruction</p>'
247
+ f'<p class="instruction-text">{safe_text}</p>'
248
+ "</div>"
249
+ )
250
+
251
+
252
+ def build_interface():
253
+ """Build Gradio interface."""
254
+ metadata, end_effector, hands_2d = load_data()
255
+
256
+ total_frames = len(metadata.get('poses', []))
257
+ fps = metadata.get('fps', 60)
258
+ hand_detection_rate = len(hands_2d) / max(1, total_frames) * 100 if total_frames > 0 else 0
259
+
260
+ left_poses = sum(1 for f in end_effector.values() if f and isinstance(f, dict) and f.get('left_hand'))
261
+ right_poses = sum(1 for f in end_effector.values() if f and isinstance(f, dict) and f.get('right_hand'))
262
+
263
+ video_path = get_data_dir() / "video.mp4"
264
+
265
+ # Build data for left hand
266
+ left_timestamps, left_state = build_state_dataframe(metadata, end_effector, "left")
267
+ left_figs = {metric: build_plot_html(left_timestamps, left_state, metric) for metric in METRIC_LABELS.keys()}
268
+
269
+ # Build data for right hand
270
+ right_timestamps, right_state = build_state_dataframe(metadata, end_effector, "right")
271
+ right_figs = {metric: build_plot_html(right_timestamps, right_state, metric) for metric in METRIC_LABELS.keys()}
272
+
273
+ stats_html = f"""
274
+ <div class="stats-card">
275
+ <ul>
276
+ <li><span>Number of samples/frames:</span> {total_frames:,}</li>
277
+ <li><span>Hand detection rate:</span> {hand_detection_rate:.1f}%</li>
278
+ <li><span>Left hand poses:</span> {left_poses}</li>
279
+ <li><span>Right hand poses:</span> {right_poses}</li>
280
+ <li><span>Frames per second:</span> {fps:.1f}</li>
281
+ </ul>
282
+ </div>
283
  """
284
+
285
+ instruction_text = "LiDAR-based egocentric hand tracking for robot training data"
286
+
287
+ theme = gr.themes.Soft(
288
+ primary_hue="cyan", secondary_hue="blue", neutral_hue="slate"
289
+ ).set(
290
+ body_background_fill="#0c1424",
291
+ body_text_color="#f8fafc",
292
+ block_background_fill="#111a2c",
293
+ block_title_text_color="#f8fafc",
294
+ input_background_fill="#151f33",
295
+ border_color_primary="#1f2b47",
296
+ shadow_drop="none",
297
+ )
298
+
299
+ with gr.Blocks(theme=theme, css=CUSTOM_CSS) as demo:
300
+ gr.Markdown("# 🤖 Dynamic Intelligence - Human Demo Visualizer")
301
+ gr.Markdown(
302
+ "Egocentric hand tracking dataset for humanoid robot training. "
303
+ "Pipeline: iPhone LiDAR → MediaPipe → 6DoF End-Effector → Robot Training Data"
304
+ )
305
+
306
+ with gr.Row(equal_height=True):
307
+ with gr.Column(scale=1, min_width=260, elem_classes=["side-panel"]):
308
+ gr.HTML(stats_html)
309
+ with gr.Column(scale=2, min_width=640, elem_classes=["main-panel"]):
310
+ instruction_box = gr.HTML(
311
+ format_instruction_html(instruction_text),
312
+ label="Language Instruction",
313
+ )
314
+ with gr.Column(elem_classes=["video-card"]):
315
+ gr.HTML('<div class="video-title">RGB Video</div>')
316
+ video = gr.Video(
317
+ height=360,
318
+ value=str(video_path) if video_path.exists() else None,
319
+ elem_classes=["video-panel"],
320
+ show_label=False,
321
+ show_download_button=False,
322
+ )
323
+ download_button = gr.DownloadButton(
324
+ label="Download",
325
+ value=str(video_path) if video_path.exists() else None,
326
+ elem_classes=["download-button"],
327
+ )
328
+
329
+ plot_outputs_left = []
330
+ gr.Markdown("### Left Hand Trajectories", elem_classes=["plots-title"])
331
+ with gr.Column(elem_classes=["plots-wrap"]):
332
+ for row in PLOT_GRID:
333
+ with gr.Row():
334
+ for metric in row:
335
+ plot = gr.HTML(value=left_figs[metric], elem_classes=["plot-html"])
336
+ plot_outputs_left.append(plot)
337
+
338
+ plot_outputs_right = []
339
+ gr.Markdown("### Right Hand Trajectories", elem_classes=["plots-title"])
340
+ with gr.Column(elem_classes=["plots-wrap"]):
341
+ for row in PLOT_GRID:
342
+ with gr.Row():
343
+ for metric in row:
344
+ plot = gr.HTML(value=right_figs[metric], elem_classes=["plot-html"])
345
+ plot_outputs_right.append(plot)
346
+
347
+ return demo
348
+
349
 
350
+ def main():
351
+ demo = build_interface()
352
+ demo.queue().launch(show_api=False)
353
 
 
 
 
354
 
355
  if __name__ == "__main__":
356
+ main()
357
 
358
+ demo = build_interface()