oxkitsune commited on
Commit
133152e
·
1 Parent(s): fc9f77a

Update to 0.23.0

Browse files
Files changed (9) hide show
  1. README.md +174 -52
  2. app.py +148 -22
  3. color_grid.py +6 -4
  4. css.css +4 -3
  5. examples/rgbd.rrd +2 -2
  6. examples/rrt-star.rrd +2 -2
  7. examples/structure_from_motion.rrd +2 -2
  8. requirements.txt +1 -1
  9. space.py +313 -59
README.md CHANGED
@@ -10,7 +10,7 @@ app_file: space.py
10
  ---
11
 
12
  # `gradio_rerun`
13
- <a href="https://pypi.org/project/gradio_rerun/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_rerun"></a> <a href="https://github.com/radames/gradio-rerun-viewer/issues" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/Issues-white?logo=github&logoColor=black"></a>
14
 
15
  Rerun viewer with Gradio
16
 
@@ -23,22 +23,37 @@ pip install gradio_rerun
23
  ## Usage
24
 
25
  ```python
26
- import cv2
 
 
 
 
 
 
 
27
  import os
28
  import tempfile
29
  import time
 
30
 
 
31
  import gradio as gr
32
- from gradio_rerun import Rerun
33
-
34
  import rerun as rr
35
  import rerun.blueprint as rrb
36
-
37
  from color_grid import build_color_grid
 
 
 
 
 
 
 
38
 
39
- # NOTE: Functions that work with Rerun should be decorated with `@rr.thread_local_stream`.
40
- # This decorator creates a generator-aware thread-local context so that rerun log calls
41
- # across multiple workers stay isolated.
 
 
42
 
43
 
44
  # A task can directly log to a binary stream, which is routed to the embedded viewer.
@@ -46,9 +61,10 @@ from color_grid import build_color_grid
46
  #
47
  # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
48
  # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
49
- @rr.thread_local_stream("rerun_example_streaming_blur")
50
- def streaming_repeated_blur(img):
51
- stream = rr.binary_stream()
 
52
 
53
  if img is None:
54
  raise gr.Error("Must provide an image to blur.")
@@ -61,23 +77,19 @@ def streaming_repeated_blur(img):
61
  collapse_panels=True,
62
  )
63
 
64
- rr.send_blueprint(blueprint)
65
-
66
- rr.set_time_sequence("iteration", 0)
67
-
68
- rr.log("image/original", rr.Image(img))
69
  yield stream.read()
70
 
71
  blur = img
72
-
73
  for i in range(100):
74
- rr.set_time_sequence("iteration", i)
75
 
76
  # Pretend blurring takes a while so we can see streaming in action.
77
  time.sleep(0.1)
78
  blur = cv2.GaussianBlur(blur, (5, 5), 0)
79
-
80
- rr.log("image/blurred", rr.Image(blur))
81
 
82
  # Each time we yield bytes from the stream back to Gradio, they
83
  # are incrementally sent to the viewer. Make sure to yield any time
@@ -85,6 +97,87 @@ def streaming_repeated_blur(img):
85
  yield stream.read()
86
 
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  # However, if you have a workflow that creates an RRD file instead, you can still send it
89
  # directly to the viewer by simply returning the path to the RRD file.
90
  #
@@ -92,12 +185,15 @@ def streaming_repeated_blur(img):
92
  # be easily modified to stream data directly via Gradio.
93
  #
94
  # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
95
- # don't accumulate too many temporary files.
96
  @rr.thread_local_stream("rerun_example_cube_rrd")
97
  def create_cube_rrd(x, y, z, pending_cleanup):
98
  cube = build_color_grid(int(x), int(y), int(z), twist=0)
99
  rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
100
 
 
 
 
101
  # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
102
  # any pending files to be cleaned up when the state is deleted.
103
  temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
@@ -111,7 +207,7 @@ def create_cube_rrd(x, y, z, pending_cleanup):
111
  return temp.name
112
 
113
 
114
- def cleanup_cube_rrds(pending_cleanup):
115
  for f in pending_cleanup:
116
  os.unlink(f)
117
 
@@ -122,6 +218,7 @@ with gr.Blocks() as demo:
122
  img = gr.Image(interactive=True, label="Image")
123
  with gr.Column():
124
  stream_blur = gr.Button("Stream Repeated Blur")
 
125
  with gr.Row():
126
  viewer = Rerun(
127
  streaming=True,
@@ -131,22 +228,35 @@ with gr.Blocks() as demo:
131
  "selection": "hidden",
132
  },
133
  )
134
- stream_blur.click(streaming_repeated_blur, inputs=[img], outputs=[viewer])
135
 
136
- with gr.Tab("Dynamic RRD"):
137
- pending_cleanup = gr.State(
138
- [], time_to_live=10, delete_callback=cleanup_cube_rrds
 
 
 
 
 
 
 
 
 
 
 
139
  )
 
 
 
 
 
 
 
 
 
140
  with gr.Row():
141
- x_count = gr.Number(
142
- minimum=1, maximum=10, value=5, precision=0, label="X Count"
143
- )
144
- y_count = gr.Number(
145
- minimum=1, maximum=10, value=5, precision=0, label="Y Count"
146
- )
147
- z_count = gr.Number(
148
- minimum=1, maximum=10, value=5, precision=0, label="Z Count"
149
- )
150
  with gr.Row():
151
  create_rrd = gr.Button("Create RRD")
152
  with gr.Row():
@@ -186,6 +296,8 @@ with gr.Blocks() as demo:
186
  },
187
  )
188
  choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
 
 
189
 
190
 
191
  if __name__ == "__main__":
@@ -216,13 +328,13 @@ list[pathlib.Path | str]
216
  | pathlib.Path
217
  | str
218
  | bytes
219
- | Callable
220
  | None
221
  ```
222
 
223
  </td>
224
  <td align="left"><code>None</code></td>
225
- <td align="left">Takes a singular or list of RRD resources. Each RRD can be a Path, a string containing a url, or a binary blob containing encoded RRD data. If callable, the function will be called whenever the app loads to set the initial value of the component.</td>
226
  </tr>
227
 
228
  <tr>
@@ -235,7 +347,7 @@ str | None
235
 
236
  </td>
237
  <td align="left"><code>None</code></td>
238
- <td align="left">The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.</td>
239
  </tr>
240
 
241
  <tr>
@@ -248,7 +360,7 @@ float | None
248
 
249
  </td>
250
  <td align="left"><code>None</code></td>
251
- <td align="left">If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute.</td>
252
  </tr>
253
 
254
  <tr>
@@ -274,7 +386,7 @@ bool
274
 
275
  </td>
276
  <td align="left"><code>True</code></td>
277
- <td align="left">If True, will place the component in a container - providing some extra padding around the border.</td>
278
  </tr>
279
 
280
  <tr>
@@ -287,7 +399,7 @@ int | None
287
 
288
  </td>
289
  <td align="left"><code>None</code></td>
290
- <td align="left">relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.</td>
291
  </tr>
292
 
293
  <tr>
@@ -300,7 +412,7 @@ int
300
 
301
  </td>
302
  <td align="left"><code>160</code></td>
303
- <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.</td>
304
  </tr>
305
 
306
  <tr>
@@ -313,7 +425,7 @@ int | str
313
 
314
  </td>
315
  <td align="left"><code>640</code></td>
316
- <td align="left">height of component in pixels. If a string is provided, will be interpreted as a CSS value. If None, will be set to 640px.</td>
317
  </tr>
318
 
319
  <tr>
@@ -339,7 +451,7 @@ bool
339
 
340
  </td>
341
  <td align="left"><code>False</code></td>
342
- <td align="left">If True, the data should be incrementally yielded from the source as `bytes` returned by calling `.read()` on an `rr.binary_stream()`</td>
343
  </tr>
344
 
345
  <tr>
@@ -352,7 +464,7 @@ str | None
352
 
353
  </td>
354
  <td align="left"><code>None</code></td>
355
- <td align="left">An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
356
  </tr>
357
 
358
  <tr>
@@ -365,7 +477,7 @@ list[str] | str | None
365
 
366
  </td>
367
  <td align="left"><code>None</code></td>
368
- <td align="left">An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.</td>
369
  </tr>
370
 
371
  <tr>
@@ -378,7 +490,7 @@ bool
378
 
379
  </td>
380
  <td align="left"><code>True</code></td>
381
- <td align="left">If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.</td>
382
  </tr>
383
 
384
  <tr>
@@ -386,16 +498,26 @@ bool
386
  <td align="left" style="width: 25%;">
387
 
388
  ```python
389
- dict[str, Any] | None
390
  ```
391
 
392
  </td>
393
  <td align="left"><code>None</code></td>
394
- <td align="left">Force viewer panels to a specific state. Any panels set cannot be toggled by the user in the viewer. Panel names are "top", "blueprint", "selection", and "time". States are "hidden", "collapsed", and "expanded".</td>
395
  </tr>
396
  </tbody></table>
397
 
398
 
 
 
 
 
 
 
 
 
 
 
399
 
400
 
401
  ### User function
@@ -407,8 +529,8 @@ The impact on the users predict function varies depending on whether the compone
407
 
408
  The code snippet below is accurate in cases where the component is used as both an input and an output.
409
 
410
- - **As output:** Is passed, a RerunData object.
411
- - **As input:** Should return, expects.
412
 
413
  ```python
414
  def predict(
@@ -421,5 +543,5 @@ The code snippet below is accurate in cases where the component is used as both
421
  ## `RerunData`
422
  ```python
423
  class RerunData(GradioRootModel):
424
- root: list[FileData | str]
425
  ```
 
10
  ---
11
 
12
  # `gradio_rerun`
13
+ <a href="https://pypi.org/project/gradio_rerun/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_rerun"></a> <a href="https://github.com/rerun-io/gradio-rerun-viewer/issues" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/Issues-white?logo=github&logoColor=black"></a> <a href="https://huggingface.co/spaces/rerun/gradio-rerun-viewer/discussions" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/%F0%9F%A4%97%20Discuss-%23097EFF?style=flat&logoColor=black"></a>
14
 
15
  Rerun viewer with Gradio
16
 
 
23
  ## Usage
24
 
25
  ```python
26
+ """
27
+ Demonstrates integrating Rerun visualization with Gradio.
28
+
29
+ Provides example implementations of data streaming, keypoint annotation, and dynamic
30
+ visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities.
31
+ """
32
+
33
+ import math
34
  import os
35
  import tempfile
36
  import time
37
+ import uuid
38
 
39
+ import cv2
40
  import gradio as gr
 
 
41
  import rerun as rr
42
  import rerun.blueprint as rrb
 
43
  from color_grid import build_color_grid
44
+ from gradio_rerun import Rerun
45
+ from gradio_rerun.events import (
46
+ SelectionChange,
47
+ TimelineChange,
48
+ TimeUpdate,
49
+ )
50
+
51
 
52
+ # Whenever we need a recording, we construct a new recording stream.
53
+ # As long as the app and recording IDs remain the same, the data
54
+ # will be merged by the Viewer.
55
+ def get_recording(recording_id: str) -> rr.RecordingStream:
56
+ return rr.RecordingStream(application_id="rerun_example_gradio", recording_id=recording_id)
57
 
58
 
59
  # A task can directly log to a binary stream, which is routed to the embedded viewer.
 
61
  #
62
  # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
63
  # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
64
+ def streaming_repeated_blur(recording_id: str, img):
65
+ # Here we get a recording using the provided recording id.
66
+ rec = get_recording(recording_id)
67
+ stream = rec.binary_stream()
68
 
69
  if img is None:
70
  raise gr.Error("Must provide an image to blur.")
 
77
  collapse_panels=True,
78
  )
79
 
80
+ rec.send_blueprint(blueprint)
81
+ rec.set_time("iteration", sequence=0)
82
+ rec.log("image/original", rr.Image(img))
 
 
83
  yield stream.read()
84
 
85
  blur = img
 
86
  for i in range(100):
87
+ rec.set_time("iteration", sequence=i)
88
 
89
  # Pretend blurring takes a while so we can see streaming in action.
90
  time.sleep(0.1)
91
  blur = cv2.GaussianBlur(blur, (5, 5), 0)
92
+ rec.log("image/blurred", rr.Image(blur))
 
93
 
94
  # Each time we yield bytes from the stream back to Gradio, they
95
  # are incrementally sent to the viewer. Make sure to yield any time
 
97
  yield stream.read()
98
 
99
 
100
+ # In this example the user is able to add keypoints to an image visualized in Rerun.
101
+ # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong
102
+ # to a specific session (https://www.gradio.app/guides/state-in-blocks).
103
+ #
104
+ # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions.
105
+ Keypoint = tuple[float, float]
106
+ keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {}
107
+
108
+
109
+ def get_keypoints_for_user_at_sequence_index(request: gr.Request, sequence: int) -> list[Keypoint]:
110
+ per_sequence = keypoints_per_session_per_sequence_index[request.session_hash]
111
+ if sequence not in per_sequence:
112
+ per_sequence[sequence] = []
113
+
114
+ return per_sequence[sequence]
115
+
116
+
117
+ def initialize_instance(request: gr.Request) -> None:
118
+ keypoints_per_session_per_sequence_index[request.session_hash] = {}
119
+
120
+
121
+ def cleanup_instance(request: gr.Request) -> None:
122
+ if request.session_hash in keypoints_per_session_per_sequence_index:
123
+ del keypoints_per_session_per_sequence_index[request.session_hash]
124
+
125
+
126
+ # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this
127
+ # event listener is fired.
128
+ #
129
+ # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata
130
+ # `gr.Request`: https://www.gradio.app/main/docs/gradio/request
131
+ def register_keypoint(
132
+ active_recording_id: str,
133
+ current_timeline: str,
134
+ current_time: float,
135
+ request: gr.Request,
136
+ change: SelectionChange,
137
+ ):
138
+ if active_recording_id == "":
139
+ return
140
+
141
+ if current_timeline != "iteration":
142
+ return
143
+
144
+ evt = change.payload
145
+
146
+ # We can only log a keypoint if the user selected only a single item.
147
+ if len(evt.items) != 1:
148
+ return
149
+ item = evt.items[0]
150
+
151
+ # If the selected item isn't an entity, or we don't have its position, then bail out.
152
+ if item.type != "entity" or item.position is None:
153
+ return
154
+
155
+ # Now we can produce a valid keypoint.
156
+ rec = get_recording(active_recording_id)
157
+ stream = rec.binary_stream()
158
+
159
+ # We round `current_time` toward 0, because that gives us the sequence index
160
+ # that the user is currently looking at, due to the Viewer's latest-at semantics.
161
+ index = math.floor(current_time)
162
+
163
+ # We keep track of the keypoints per sequence index for each user manually.
164
+ keypoints = get_keypoints_for_user_at_sequence_index(request, index)
165
+ keypoints.append(item.position[0:2])
166
+
167
+ rec.set_time("iteration", sequence=index)
168
+ rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2))
169
+
170
+ yield stream.read()
171
+
172
+
173
+ def track_current_time(evt: TimeUpdate):
174
+ return evt.payload.time
175
+
176
+
177
+ def track_current_timeline_and_time(evt: TimelineChange):
178
+ return evt.payload.timeline, evt.payload.time
179
+
180
+
181
  # However, if you have a workflow that creates an RRD file instead, you can still send it
182
  # directly to the viewer by simply returning the path to the RRD file.
183
  #
 
185
  # be easily modified to stream data directly via Gradio.
186
  #
187
  # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
188
+ # don't accumulate too many temporary files.
189
  @rr.thread_local_stream("rerun_example_cube_rrd")
190
  def create_cube_rrd(x, y, z, pending_cleanup):
191
  cube = build_color_grid(int(x), int(y), int(z), twist=0)
192
  rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
193
 
194
+ # Simulate delay
195
+ time.sleep(x / 10)
196
+
197
  # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
198
  # any pending files to be cleaned up when the state is deleted.
199
  temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
 
207
  return temp.name
208
 
209
 
210
+ def cleanup_cube_rrds(pending_cleanup: list[str]) -> None:
211
  for f in pending_cleanup:
212
  os.unlink(f)
213
 
 
218
  img = gr.Image(interactive=True, label="Image")
219
  with gr.Column():
220
  stream_blur = gr.Button("Stream Repeated Blur")
221
+
222
  with gr.Row():
223
  viewer = Rerun(
224
  streaming=True,
 
228
  "selection": "hidden",
229
  },
230
  )
 
231
 
232
+ # We make a new recording id, and store it in a Gradio's session state.
233
+ recording_id = gr.State(uuid.uuid4())
234
+
235
+ # Also store the current timeline and time of the viewer in the session state.
236
+ current_timeline = gr.State("")
237
+ current_time = gr.State(0.0)
238
+
239
+ # When registering the event listeners, we pass the `recording_id` in as input in order to create
240
+ # a recording stream using that id.
241
+ stream_blur.click(
242
+ # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback.
243
+ streaming_repeated_blur,
244
+ inputs=[recording_id, img],
245
+ outputs=[viewer],
246
  )
247
+ viewer.selection_change(
248
+ register_keypoint,
249
+ inputs=[recording_id, current_timeline, current_time],
250
+ outputs=[viewer],
251
+ )
252
+ viewer.time_update(track_current_time, outputs=[current_time])
253
+ viewer.timeline_change(track_current_timeline_and_time, outputs=[current_timeline, current_time])
254
+ with gr.Tab("Dynamic RRD"):
255
+ pending_cleanup = gr.State([], time_to_live=10, delete_callback=cleanup_cube_rrds)
256
  with gr.Row():
257
+ x_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="X Count")
258
+ y_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Y Count")
259
+ z_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Z Count")
 
 
 
 
 
 
260
  with gr.Row():
261
  create_rrd = gr.Button("Create RRD")
262
  with gr.Row():
 
296
  },
297
  )
298
  choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
299
+ demo.load(initialize_instance)
300
+ demo.close(cleanup_instance)
301
 
302
 
303
  if __name__ == "__main__":
 
328
  | pathlib.Path
329
  | str
330
  | bytes
331
+ | collections.abc.Callable
332
  | None
333
  ```
334
 
335
  </td>
336
  <td align="left"><code>None</code></td>
337
+ <td align="left">Takes a singular or list of RRD resources. Each RRD can be a Path, a string containing a url,</td>
338
  </tr>
339
 
340
  <tr>
 
347
 
348
  </td>
349
  <td align="left"><code>None</code></td>
350
+ <td align="left">The label for this component. Appears above the component and is also used as the header if there</td>
351
  </tr>
352
 
353
  <tr>
 
360
 
361
  </td>
362
  <td align="left"><code>None</code></td>
363
+ <td align="left">If `value` is a callable, run the function 'every' number of seconds while the client connection is</td>
364
  </tr>
365
 
366
  <tr>
 
386
 
387
  </td>
388
  <td align="left"><code>True</code></td>
389
+ <td align="left">If True, will place the component in a container providing some extra padding around the border.</td>
390
  </tr>
391
 
392
  <tr>
 
399
 
400
  </td>
401
  <td align="left"><code>None</code></td>
402
+ <td align="left">relative size compared to adjacent Components.</td>
403
  </tr>
404
 
405
  <tr>
 
412
 
413
  </td>
414
  <td align="left"><code>160</code></td>
415
+ <td align="left">minimum pixel width, will wrap if not sufficient screen space to satisfy this value.</td>
416
  </tr>
417
 
418
  <tr>
 
425
 
426
  </td>
427
  <td align="left"><code>640</code></td>
428
+ <td align="left">height of component in pixels. If a string is provided, will be interpreted as a CSS value.</td>
429
  </tr>
430
 
431
  <tr>
 
451
 
452
  </td>
453
  <td align="left"><code>False</code></td>
454
+ <td align="left">If True, the data should be incrementally yielded from the source as `bytes` returned by</td>
455
  </tr>
456
 
457
  <tr>
 
464
 
465
  </td>
466
  <td align="left"><code>None</code></td>
467
+ <td align="left">An optional string that is assigned as the id of this component in the HTML DOM.</td>
468
  </tr>
469
 
470
  <tr>
 
477
 
478
  </td>
479
  <td align="left"><code>None</code></td>
480
+ <td align="left">An optional list of strings that are assigned as the classes of this component in</td>
481
  </tr>
482
 
483
  <tr>
 
490
 
491
  </td>
492
  <td align="left"><code>True</code></td>
493
+ <td align="left">If False, component will not render be rendered in the Blocks context.</td>
494
  </tr>
495
 
496
  <tr>
 
498
  <td align="left" style="width: 25%;">
499
 
500
  ```python
501
+ dict[str, typing.Any] | None
502
  ```
503
 
504
  </td>
505
  <td align="left"><code>None</code></td>
506
+ <td align="left">Force viewer panels to a specific state.</td>
507
  </tr>
508
  </tbody></table>
509
 
510
 
511
+ ### Events
512
+
513
+ | name | description |
514
+ | :----------------- | :------------------------------------------------------------------------------------------------------------------ |
515
+ | `play` | Fired when timeline playback starts. Callback should accept a parameter of type `gradio_rerun.events.Play` |
516
+ | `pause` | Fired when timeline pauseback starts. Callback should accept a parameter of type `gradio_rerun.events.Pause` |
517
+ | `time_update` | Fired when time updates. Callback should accept a parameter of type `gradio_rerun.events.TimeUpdate`. |
518
+ | `timeline_change` | Fired when a timeline is selected. Callback should accept a parameter of type `gradio_rerun.events.TimelineChange`. |
519
+ | `selection_change` | Fired when the selection changes. Callback should accept a parameter of type `gradio_rerun.events.SelectionChange`. |
520
+
521
 
522
 
523
  ### User function
 
529
 
530
  The code snippet below is accurate in cases where the component is used as both an input and an output.
531
 
532
+ - **As output:** Is passed, a `RerunData` object.
533
+ - **As input:** Should return, the value to send over to the Rerun viewer on the front-end.
534
 
535
  ```python
536
  def predict(
 
543
  ## `RerunData`
544
  ```python
545
  class RerunData(GradioRootModel):
546
+ root: Sequence[FileData | Path | str] | None
547
  ```
app.py CHANGED
@@ -1,19 +1,36 @@
1
- import cv2
 
 
 
 
 
 
 
2
  import os
3
  import tempfile
4
  import time
 
5
 
 
6
  import gradio as gr
7
- from gradio_rerun import Rerun
8
-
9
  import rerun as rr
10
  import rerun.blueprint as rrb
11
-
12
  from color_grid import build_color_grid
 
 
 
 
 
 
 
13
 
14
- # NOTE: Functions that work with Rerun should be decorated with `@rr.thread_local_stream`.
15
- # This decorator creates a generator-aware thread-local context so that rerun log calls
16
- # across multiple workers stay isolated.
 
 
 
 
17
 
18
 
19
  # A task can directly log to a binary stream, which is routed to the embedded viewer.
@@ -21,9 +38,10 @@ from color_grid import build_color_grid
21
  #
22
  # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
23
  # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
24
- @rr.thread_local_stream("rerun_example_streaming_blur")
25
- def streaming_repeated_blur(img):
26
- stream = rr.binary_stream()
 
27
 
28
  if img is None:
29
  raise gr.Error("Must provide an image to blur.")
@@ -36,23 +54,19 @@ def streaming_repeated_blur(img):
36
  collapse_panels=True,
37
  )
38
 
39
- rr.send_blueprint(blueprint)
40
-
41
- rr.set_time_sequence("iteration", 0)
42
-
43
- rr.log("image/original", rr.Image(img))
44
  yield stream.read()
45
 
46
  blur = img
47
-
48
  for i in range(100):
49
- rr.set_time_sequence("iteration", i)
50
 
51
  # Pretend blurring takes a while so we can see streaming in action.
52
  time.sleep(0.1)
53
  blur = cv2.GaussianBlur(blur, (5, 5), 0)
54
-
55
- rr.log("image/blurred", rr.Image(blur))
56
 
57
  # Each time we yield bytes from the stream back to Gradio, they
58
  # are incrementally sent to the viewer. Make sure to yield any time
@@ -60,6 +74,89 @@ def streaming_repeated_blur(img):
60
  yield stream.read()
61
 
62
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  # However, if you have a workflow that creates an RRD file instead, you can still send it
64
  # directly to the viewer by simply returning the path to the RRD file.
65
  #
@@ -67,12 +164,15 @@ def streaming_repeated_blur(img):
67
  # be easily modified to stream data directly via Gradio.
68
  #
69
  # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
70
- # don't accumulate too many temporary files.
71
  @rr.thread_local_stream("rerun_example_cube_rrd")
72
  def create_cube_rrd(x, y, z, pending_cleanup):
73
  cube = build_color_grid(int(x), int(y), int(z), twist=0)
74
  rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
75
 
 
 
 
76
  # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
77
  # any pending files to be cleaned up when the state is deleted.
78
  temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
@@ -86,7 +186,7 @@ def create_cube_rrd(x, y, z, pending_cleanup):
86
  return temp.name
87
 
88
 
89
- def cleanup_cube_rrds(pending_cleanup):
90
  for f in pending_cleanup:
91
  os.unlink(f)
92
 
@@ -97,6 +197,7 @@ with gr.Blocks() as demo:
97
  img = gr.Image(interactive=True, label="Image")
98
  with gr.Column():
99
  stream_blur = gr.Button("Stream Repeated Blur")
 
100
  with gr.Row():
101
  viewer = Rerun(
102
  streaming=True,
@@ -106,8 +207,31 @@ with gr.Blocks() as demo:
106
  "selection": "hidden",
107
  },
108
  )
109
- stream_blur.click(streaming_repeated_blur, inputs=[img], outputs=[viewer])
110
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  with gr.Tab("Dynamic RRD"):
112
  pending_cleanup = gr.State(
113
  [], time_to_live=10, delete_callback=cleanup_cube_rrds
@@ -161,6 +285,8 @@ with gr.Blocks() as demo:
161
  },
162
  )
163
  choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
 
 
164
 
165
 
166
  if __name__ == "__main__":
 
1
+ """
2
+ Demonstrates integrating Rerun visualization with Gradio.
3
+
4
+ Provides example implementations of data streaming, keypoint annotation, and dynamic
5
+ visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities.
6
+ """
7
+
8
+ import math
9
  import os
10
  import tempfile
11
  import time
12
+ import uuid
13
 
14
+ import cv2
15
  import gradio as gr
 
 
16
  import rerun as rr
17
  import rerun.blueprint as rrb
 
18
  from color_grid import build_color_grid
19
+ from gradio_rerun import Rerun
20
+ from gradio_rerun.events import (
21
+ SelectionChange,
22
+ TimelineChange,
23
+ TimeUpdate,
24
+ )
25
+
26
 
27
+ # Whenever we need a recording, we construct a new recording stream.
28
+ # As long as the app and recording IDs remain the same, the data
29
+ # will be merged by the Viewer.
30
+ def get_recording(recording_id: str) -> rr.RecordingStream:
31
+ return rr.RecordingStream(
32
+ application_id="rerun_example_gradio", recording_id=recording_id
33
+ )
34
 
35
 
36
  # A task can directly log to a binary stream, which is routed to the embedded viewer.
 
38
  #
39
  # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
40
  # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
41
+ def streaming_repeated_blur(recording_id: str, img):
42
+ # Here we get a recording using the provided recording id.
43
+ rec = get_recording(recording_id)
44
+ stream = rec.binary_stream()
45
 
46
  if img is None:
47
  raise gr.Error("Must provide an image to blur.")
 
54
  collapse_panels=True,
55
  )
56
 
57
+ rec.send_blueprint(blueprint)
58
+ rec.set_time("iteration", sequence=0)
59
+ rec.log("image/original", rr.Image(img))
 
 
60
  yield stream.read()
61
 
62
  blur = img
 
63
  for i in range(100):
64
+ rec.set_time("iteration", sequence=i)
65
 
66
  # Pretend blurring takes a while so we can see streaming in action.
67
  time.sleep(0.1)
68
  blur = cv2.GaussianBlur(blur, (5, 5), 0)
69
+ rec.log("image/blurred", rr.Image(blur))
 
70
 
71
  # Each time we yield bytes from the stream back to Gradio, they
72
  # are incrementally sent to the viewer. Make sure to yield any time
 
74
  yield stream.read()
75
 
76
 
77
+ # In this example the user is able to add keypoints to an image visualized in Rerun.
78
+ # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong
79
+ # to a specific session (https://www.gradio.app/guides/state-in-blocks).
80
+ #
81
+ # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions.
82
+ Keypoint = tuple[float, float]
83
+ keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {}
84
+
85
+
86
+ def get_keypoints_for_user_at_sequence_index(
87
+ request: gr.Request, sequence: int
88
+ ) -> list[Keypoint]:
89
+ per_sequence = keypoints_per_session_per_sequence_index[request.session_hash]
90
+ if sequence not in per_sequence:
91
+ per_sequence[sequence] = []
92
+
93
+ return per_sequence[sequence]
94
+
95
+
96
+ def initialize_instance(request: gr.Request) -> None:
97
+ keypoints_per_session_per_sequence_index[request.session_hash] = {}
98
+
99
+
100
+ def cleanup_instance(request: gr.Request) -> None:
101
+ if request.session_hash in keypoints_per_session_per_sequence_index:
102
+ del keypoints_per_session_per_sequence_index[request.session_hash]
103
+
104
+
105
+ # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this
106
+ # event listener is fired.
107
+ #
108
+ # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata
109
+ # `gr.Request`: https://www.gradio.app/main/docs/gradio/request
110
+ def register_keypoint(
111
+ active_recording_id: str,
112
+ current_timeline: str,
113
+ current_time: float,
114
+ request: gr.Request,
115
+ change: SelectionChange,
116
+ ):
117
+ if active_recording_id == "":
118
+ return
119
+
120
+ if current_timeline != "iteration":
121
+ return
122
+
123
+ evt = change.payload
124
+
125
+ # We can only log a keypoint if the user selected only a single item.
126
+ if len(evt.items) != 1:
127
+ return
128
+ item = evt.items[0]
129
+
130
+ # If the selected item isn't an entity, or we don't have its position, then bail out.
131
+ if item.type != "entity" or item.position is None:
132
+ return
133
+
134
+ # Now we can produce a valid keypoint.
135
+ rec = get_recording(active_recording_id)
136
+ stream = rec.binary_stream()
137
+
138
+ # We round `current_time` toward 0, because that gives us the sequence index
139
+ # that the user is currently looking at, due to the Viewer's latest-at semantics.
140
+ index = math.floor(current_time)
141
+
142
+ # We keep track of the keypoints per sequence index for each user manually.
143
+ keypoints = get_keypoints_for_user_at_sequence_index(request, index)
144
+ keypoints.append(item.position[0:2])
145
+
146
+ rec.set_time("iteration", sequence=index)
147
+ rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2))
148
+
149
+ yield stream.read()
150
+
151
+
152
+ def track_current_time(evt: TimeUpdate):
153
+ return evt.payload.time
154
+
155
+
156
+ def track_current_timeline_and_time(evt: TimelineChange):
157
+ return evt.payload.timeline, evt.payload.time
158
+
159
+
160
  # However, if you have a workflow that creates an RRD file instead, you can still send it
161
  # directly to the viewer by simply returning the path to the RRD file.
162
  #
 
164
  # be easily modified to stream data directly via Gradio.
165
  #
166
  # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
167
+ # don't accumulate too many temporary files.
168
  @rr.thread_local_stream("rerun_example_cube_rrd")
169
  def create_cube_rrd(x, y, z, pending_cleanup):
170
  cube = build_color_grid(int(x), int(y), int(z), twist=0)
171
  rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
172
 
173
+ # Simulate delay
174
+ time.sleep(x / 10)
175
+
176
  # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
177
  # any pending files to be cleaned up when the state is deleted.
178
  temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
 
186
  return temp.name
187
 
188
 
189
+ def cleanup_cube_rrds(pending_cleanup: list[str]) -> None:
190
  for f in pending_cleanup:
191
  os.unlink(f)
192
 
 
197
  img = gr.Image(interactive=True, label="Image")
198
  with gr.Column():
199
  stream_blur = gr.Button("Stream Repeated Blur")
200
+
201
  with gr.Row():
202
  viewer = Rerun(
203
  streaming=True,
 
207
  "selection": "hidden",
208
  },
209
  )
 
210
 
211
+ # We make a new recording id, and store it in a Gradio's session state.
212
+ recording_id = gr.State(uuid.uuid4())
213
+
214
+ # Also store the current timeline and time of the viewer in the session state.
215
+ current_timeline = gr.State("")
216
+ current_time = gr.State(0.0)
217
+
218
+ # When registering the event listeners, we pass the `recording_id` in as input in order to create
219
+ # a recording stream using that id.
220
+ stream_blur.click(
221
+ # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback.
222
+ streaming_repeated_blur,
223
+ inputs=[recording_id, img],
224
+ outputs=[viewer],
225
+ )
226
+ viewer.selection_change(
227
+ register_keypoint,
228
+ inputs=[recording_id, current_timeline, current_time],
229
+ outputs=[viewer],
230
+ )
231
+ viewer.time_update(track_current_time, outputs=[current_time])
232
+ viewer.timeline_change(
233
+ track_current_timeline_and_time, outputs=[current_timeline, current_time]
234
+ )
235
  with gr.Tab("Dynamic RRD"):
236
  pending_cleanup = gr.State(
237
  [], time_to_live=10, delete_callback=cleanup_cube_rrds
 
285
  },
286
  )
287
  choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
288
+ demo.load(initialize_instance)
289
+ demo.close(cleanup_instance)
290
 
291
 
292
  if __name__ == "__main__":
color_grid.py CHANGED
@@ -1,11 +1,14 @@
1
- import numpy as np
2
  from math import cos, sin
3
- from collections import namedtuple
 
4
 
5
  ColorGrid = namedtuple("ColorGrid", ["positions", "colors"])
6
 
7
 
8
- def build_color_grid(x_count=10, y_count=10, z_count=10, twist=0):
 
 
9
  """
10
  Create a cube of points with colors.
11
 
@@ -19,7 +22,6 @@ def build_color_grid(x_count=10, y_count=10, z_count=10, twist=0):
19
  Angle to twist from bottom to top of the cube
20
 
21
  """
22
-
23
  grid = np.mgrid[
24
  slice(-x_count, x_count, x_count * 1j),
25
  slice(-y_count, y_count, y_count * 1j),
 
1
+ from collections import namedtuple # noqa: D100
2
  from math import cos, sin
3
+
4
+ import numpy as np
5
 
6
  ColorGrid = namedtuple("ColorGrid", ["positions", "colors"])
7
 
8
 
9
+ def build_color_grid(
10
+ x_count: int = 10, y_count: int = 10, z_count: int = 10, twist: int = 0
11
+ ) -> ColorGrid:
12
  """
13
  Create a cube of points with colors.
14
 
 
22
  Angle to twist from bottom to top of the cube
23
 
24
  """
 
25
  grid = np.mgrid[
26
  slice(-x_count, x_count, x_count * 1j),
27
  slice(-y_count, y_count, y_count * 1j),
css.css CHANGED
@@ -135,11 +135,11 @@ h6 {
135
  letter-spacing: 0px !important;
136
  }
137
 
138
- #start .md > *:first-child {
139
  margin-top: 0;
140
  }
141
 
142
- h2 + h3 {
143
  margin-top: 0;
144
  }
145
 
@@ -148,10 +148,11 @@ h2 + h3 {
148
  border-top: 1px solid var(--block-border-color);
149
  margin: var(--vspace-2) 0 var(--vspace-2) 0;
150
  }
 
151
  .prose ul {
152
  margin: var(--vspace-2) 0 var(--vspace-1) 0;
153
  }
154
 
155
  .gap {
156
  gap: 0;
157
- }
 
135
  letter-spacing: 0px !important;
136
  }
137
 
138
+ #start .md>*:first-child {
139
  margin-top: 0;
140
  }
141
 
142
+ h2+h3 {
143
  margin-top: 0;
144
  }
145
 
 
148
  border-top: 1px solid var(--block-border-color);
149
  margin: var(--vspace-2) 0 var(--vspace-2) 0;
150
  }
151
+
152
  .prose ul {
153
  margin: var(--vspace-2) 0 var(--vspace-1) 0;
154
  }
155
 
156
  .gap {
157
  gap: 0;
158
+ }
examples/rgbd.rrd CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:23a374ab6f973424d2e6d40d54c90a5898c4ae4c3909987167fc4e349e9f7b35
3
- size 38623309
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:62a765f41f2779e7975290773ae2f24131eca986e9f7c9b829fbc0020900c422
3
+ size 18464608
examples/rrt-star.rrd CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:d26f9f5343552292208d232f2480ac24a7a9cebb331ba8ce4fa8ef3a44f5c283
3
- size 10487798
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5c7f1b7882df70a58e8544718941550a5dbf9252502b448e0a0318150b2d1442
3
+ size 6173062
examples/structure_from_motion.rrd CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:c6b4aae56c08714b4aa230cb2c75682eef8e2e6de1c2ea636e15e4eca0f9e26f
3
- size 7175301
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:3a2c5f3808f43cc42d8f06587157ed87f555cb8ac363d65cc2fbe15521e21618
3
+ size 6898468
requirements.txt CHANGED
@@ -1,2 +1,2 @@
1
- gradio_rerun
2
  opencv-python
 
1
+ gradio_rerun>=0.23.0
2
  opencv-python
space.py CHANGED
@@ -1,9 +1,134 @@
1
-
2
  import gradio as gr
3
  from app import demo as app
4
  import os
5
 
6
- _docs = {'Rerun': {'description': 'Creates a Rerun viewer component that can be used to display the output of a Rerun stream.', 'members': {'__init__': {'value': {'type': 'list[pathlib.Path | str]\n | pathlib.Path\n | str\n | bytes\n | Callable\n | None', 'default': 'None', 'description': 'Takes a singular or list of RRD resources. Each RRD can be a Path, a string containing a url, or a binary blob containing encoded RRD data. If callable, the function will be called whenever the app loads to set the initial value of the component.'}, 'label': {'type': 'str | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'every': {'type': 'float | None', 'default': 'None', 'description': "If `value` is a callable, run the function 'every' number of seconds while the client connection is open. Has no effect otherwise. Queue must be enabled. The event can be accessed (e.g. to cancel it) via this component's .load_event attribute."}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'height': {'type': 'int | str', 'default': '640', 'description': 'height of component in pixels. If a string is provided, will be interpreted as a CSS value. If None, will be set to 640px.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'streaming': {'type': 'bool', 'default': 'False', 'description': 'If True, the data should be incrementally yielded from the source as `bytes` returned by calling `.read()` on an `rr.binary_stream()`'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'panel_states': {'type': 'dict[str, Any] | None', 'default': 'None', 'description': 'Force viewer panels to a specific state. Any panels set cannot be toggled by the user in the viewer. Panel names are "top", "blueprint", "selection", and "time". States are "hidden", "collapsed", and "expanded".'}}, 'postprocess': {'value': {'type': 'list[pathlib.Path | str] | pathlib.Path | str | bytes', 'description': 'Expects'}}, 'preprocess': {'return': {'type': 'RerunData | None', 'description': 'A RerunData object.'}, 'value': None}}, 'events': {}}, '__meta__': {'additional_interfaces': {'RerunData': {'source': 'class RerunData(GradioRootModel):\n root: list[FileData | str]'}}, 'user_fn_refs': {'Rerun': ['RerunData']}}}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
 
8
  abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
 
@@ -17,18 +142,21 @@ with gr.Blocks(
17
  ),
18
  ) as demo:
19
  gr.Markdown(
20
- """
21
  # `gradio_rerun`
22
 
23
  <div style="display: flex; gap: 7px;">
24
- <a href="https://pypi.org/project/gradio_rerun/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_rerun"></a> <a href="https://github.com/radames/gradio-rerun-viewer/issues" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/Issues-white?logo=github&logoColor=black"></a>
25
  </div>
26
 
27
  Rerun viewer with Gradio
28
- """, elem_classes=["md-custom"], header_links=True)
 
 
 
29
  app.render()
30
  gr.Markdown(
31
- """
32
  ## Installation
33
 
34
  ```bash
@@ -38,22 +166,37 @@ pip install gradio_rerun
38
  ## Usage
39
 
40
  ```python
41
- import cv2
 
 
 
 
 
 
 
42
  import os
43
  import tempfile
44
  import time
 
45
 
 
46
  import gradio as gr
47
- from gradio_rerun import Rerun
48
-
49
  import rerun as rr
50
  import rerun.blueprint as rrb
51
-
52
  from color_grid import build_color_grid
 
 
 
 
 
 
53
 
54
- # NOTE: Functions that work with Rerun should be decorated with `@rr.thread_local_stream`.
55
- # This decorator creates a generator-aware thread-local context so that rerun log calls
56
- # across multiple workers stay isolated.
 
 
 
57
 
58
 
59
  # A task can directly log to a binary stream, which is routed to the embedded viewer.
@@ -61,9 +204,10 @@ from color_grid import build_color_grid
61
  #
62
  # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
63
  # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
64
- @rr.thread_local_stream("rerun_example_streaming_blur")
65
- def streaming_repeated_blur(img):
66
- stream = rr.binary_stream()
 
67
 
68
  if img is None:
69
  raise gr.Error("Must provide an image to blur.")
@@ -76,23 +220,19 @@ def streaming_repeated_blur(img):
76
  collapse_panels=True,
77
  )
78
 
79
- rr.send_blueprint(blueprint)
80
-
81
- rr.set_time_sequence("iteration", 0)
82
-
83
- rr.log("image/original", rr.Image(img))
84
  yield stream.read()
85
 
86
  blur = img
87
-
88
  for i in range(100):
89
- rr.set_time_sequence("iteration", i)
90
 
91
  # Pretend blurring takes a while so we can see streaming in action.
92
  time.sleep(0.1)
93
  blur = cv2.GaussianBlur(blur, (5, 5), 0)
94
-
95
- rr.log("image/blurred", rr.Image(blur))
96
 
97
  # Each time we yield bytes from the stream back to Gradio, they
98
  # are incrementally sent to the viewer. Make sure to yield any time
@@ -100,6 +240,87 @@ def streaming_repeated_blur(img):
100
  yield stream.read()
101
 
102
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  # However, if you have a workflow that creates an RRD file instead, you can still send it
104
  # directly to the viewer by simply returning the path to the RRD file.
105
  #
@@ -107,12 +328,15 @@ def streaming_repeated_blur(img):
107
  # be easily modified to stream data directly via Gradio.
108
  #
109
  # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
110
- # don't accumulate too many temporary files.
111
  @rr.thread_local_stream("rerun_example_cube_rrd")
112
  def create_cube_rrd(x, y, z, pending_cleanup):
113
  cube = build_color_grid(int(x), int(y), int(z), twist=0)
114
  rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
115
 
 
 
 
116
  # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
117
  # any pending files to be cleaned up when the state is deleted.
118
  temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
@@ -126,7 +350,7 @@ def create_cube_rrd(x, y, z, pending_cleanup):
126
  return temp.name
127
 
128
 
129
- def cleanup_cube_rrds(pending_cleanup):
130
  for f in pending_cleanup:
131
  os.unlink(f)
132
 
@@ -137,6 +361,7 @@ with gr.Blocks() as demo:
137
  img = gr.Image(interactive=True, label="Image")
138
  with gr.Column():
139
  stream_blur = gr.Button("Stream Repeated Blur")
 
140
  with gr.Row():
141
  viewer = Rerun(
142
  streaming=True,
@@ -146,22 +371,35 @@ with gr.Blocks() as demo:
146
  "selection": "hidden",
147
  },
148
  )
149
- stream_blur.click(streaming_repeated_blur, inputs=[img], outputs=[viewer])
150
 
151
- with gr.Tab("Dynamic RRD"):
152
- pending_cleanup = gr.State(
153
- [], time_to_live=10, delete_callback=cleanup_cube_rrds
 
 
 
 
 
 
 
 
 
 
 
154
  )
 
 
 
 
 
 
 
 
 
155
  with gr.Row():
156
- x_count = gr.Number(
157
- minimum=1, maximum=10, value=5, precision=0, label="X Count"
158
- )
159
- y_count = gr.Number(
160
- minimum=1, maximum=10, value=5, precision=0, label="Y Count"
161
- )
162
- z_count = gr.Number(
163
- minimum=1, maximum=10, value=5, precision=0, label="Z Count"
164
- )
165
  with gr.Row():
166
  create_rrd = gr.Button("Create RRD")
167
  with gr.Row():
@@ -201,27 +439,36 @@ with gr.Blocks() as demo:
201
  },
202
  )
203
  choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
 
 
204
 
205
 
206
  if __name__ == "__main__":
207
  demo.launch()
208
 
209
  ```
210
- """, elem_classes=["md-custom"], header_links=True)
211
-
 
 
212
 
213
- gr.Markdown("""
 
214
  ## `Rerun`
215
 
216
  ### Initialization
217
- """, elem_classes=["md-custom"], header_links=True)
218
-
219
- gr.ParamViewer(value=_docs["Rerun"]["members"]["__init__"], linkify=['RerunData'])
220
-
221
 
 
222
 
 
 
223
 
224
- gr.Markdown("""
 
225
 
226
  ### User function
227
 
@@ -232,8 +479,8 @@ The impact on the users predict function varies depending on whether the compone
232
 
233
  The code snippet below is accurate in cases where the component is used as both an input and an output.
234
 
235
- - **As input:** Is passed, a RerunData object.
236
- - **As output:** Should return, expects.
237
 
238
  ```python
239
  def predict(
@@ -241,19 +488,25 @@ def predict(
241
  ) -> list[pathlib.Path | str] | pathlib.Path | str | bytes:
242
  return value
243
  ```
244
- """, elem_classes=["md-custom", "Rerun-user-fn"], header_links=True)
245
-
246
-
247
-
248
 
249
- code_RerunData = gr.Markdown("""
 
250
  ## `RerunData`
251
  ```python
252
  class RerunData(GradioRootModel):
253
- root: list[FileData | str]
254
- ```""", elem_classes=["md-custom", "RerunData"], header_links=True)
 
 
 
255
 
256
- demo.load(None, js=r"""function() {
 
 
257
  const refs = {
258
  RerunData: [], };
259
  const user_fn_refs = {
@@ -288,6 +541,7 @@ class RerunData(GradioRootModel):
288
  })
289
  }
290
 
291
- """)
 
292
 
293
  demo.launch()
 
 
1
  import gradio as gr
2
  from app import demo as app
3
  import os
4
 
5
+ _docs = {
6
+ "Rerun": {
7
+ "description": "Creates a Rerun viewer component that can be used to display the output of a Rerun stream.",
8
+ "members": {
9
+ "__init__": {
10
+ "value": {
11
+ "type": "list[pathlib.Path | str]\n | pathlib.Path\n | str\n | bytes\n | collections.abc.Callable\n | None",
12
+ "default": "None",
13
+ "description": "Takes a singular or list of RRD resources. Each RRD can be a Path, a string containing a url,",
14
+ },
15
+ "label": {
16
+ "type": "str | None",
17
+ "default": "None",
18
+ "description": "The label for this component. Appears above the component and is also used as the header if there",
19
+ },
20
+ "every": {
21
+ "type": "float | None",
22
+ "default": "None",
23
+ "description": "If `value` is a callable, run the function 'every' number of seconds while the client connection is",
24
+ },
25
+ "show_label": {
26
+ "type": "bool | None",
27
+ "default": "None",
28
+ "description": "if True, will display label.",
29
+ },
30
+ "container": {
31
+ "type": "bool",
32
+ "default": "True",
33
+ "description": "If True, will place the component in a container providing some extra padding around the border.",
34
+ },
35
+ "scale": {
36
+ "type": "int | None",
37
+ "default": "None",
38
+ "description": "relative size compared to adjacent Components.",
39
+ },
40
+ "min_width": {
41
+ "type": "int",
42
+ "default": "160",
43
+ "description": "minimum pixel width, will wrap if not sufficient screen space to satisfy this value.",
44
+ },
45
+ "height": {
46
+ "type": "int | str",
47
+ "default": "640",
48
+ "description": "height of component in pixels. If a string is provided, will be interpreted as a CSS value.",
49
+ },
50
+ "visible": {
51
+ "type": "bool",
52
+ "default": "True",
53
+ "description": "If False, component will be hidden.",
54
+ },
55
+ "streaming": {
56
+ "type": "bool",
57
+ "default": "False",
58
+ "description": "If True, the data should be incrementally yielded from the source as `bytes` returned by",
59
+ },
60
+ "elem_id": {
61
+ "type": "str | None",
62
+ "default": "None",
63
+ "description": "An optional string that is assigned as the id of this component in the HTML DOM.",
64
+ },
65
+ "elem_classes": {
66
+ "type": "list[str] | str | None",
67
+ "default": "None",
68
+ "description": "An optional list of strings that are assigned as the classes of this component in",
69
+ },
70
+ "render": {
71
+ "type": "bool",
72
+ "default": "True",
73
+ "description": "If False, component will not render be rendered in the Blocks context.",
74
+ },
75
+ "panel_states": {
76
+ "type": "dict[str, typing.Any] | None",
77
+ "default": "None",
78
+ "description": "Force viewer panels to a specific state.",
79
+ },
80
+ },
81
+ "postprocess": {
82
+ "value": {
83
+ "type": "list[pathlib.Path | str] | pathlib.Path | str | bytes",
84
+ "description": "The value to send over to the Rerun viewer on the front-end.",
85
+ }
86
+ },
87
+ "preprocess": {
88
+ "return": {
89
+ "type": "RerunData | None",
90
+ "description": "A `RerunData` object.",
91
+ },
92
+ "value": None,
93
+ },
94
+ },
95
+ "events": {
96
+ "play": {
97
+ "type": None,
98
+ "default": None,
99
+ "description": "Fired when timeline playback starts. Callback should accept a parameter of type `gradio_rerun.events.Play`",
100
+ },
101
+ "pause": {
102
+ "type": None,
103
+ "default": None,
104
+ "description": "Fired when timeline pauseback starts. Callback should accept a parameter of type `gradio_rerun.events.Pause`",
105
+ },
106
+ "time_update": {
107
+ "type": None,
108
+ "default": None,
109
+ "description": "Fired when time updates. Callback should accept a parameter of type `gradio_rerun.events.TimeUpdate`.",
110
+ },
111
+ "timeline_change": {
112
+ "type": None,
113
+ "default": None,
114
+ "description": "Fired when a timeline is selected. Callback should accept a parameter of type `gradio_rerun.events.TimelineChange`.",
115
+ },
116
+ "selection_change": {
117
+ "type": None,
118
+ "default": None,
119
+ "description": "Fired when the selection changes. Callback should accept a parameter of type `gradio_rerun.events.SelectionChange`.",
120
+ },
121
+ },
122
+ },
123
+ "__meta__": {
124
+ "additional_interfaces": {
125
+ "RerunData": {
126
+ "source": "class RerunData(GradioRootModel):\n root: Sequence[FileData | Path | str] | None"
127
+ }
128
+ },
129
+ "user_fn_refs": {"Rerun": ["RerunData"]},
130
+ },
131
+ }
132
 
133
  abs_path = os.path.join(os.path.dirname(__file__), "css.css")
134
 
 
142
  ),
143
  ) as demo:
144
  gr.Markdown(
145
+ """
146
  # `gradio_rerun`
147
 
148
  <div style="display: flex; gap: 7px;">
149
+ <a href="https://pypi.org/project/gradio_rerun/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_rerun"></a> <a href="https://github.com/rerun-io/gradio-rerun-viewer/issues" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/Issues-white?logo=github&logoColor=black"></a> <a href="https://huggingface.co/spaces/rerun/gradio-rerun-viewer/discussions" target="_blank"><img alt="Static Badge" src="https://img.shields.io/badge/%F0%9F%A4%97%20Discuss-%23097EFF?style=flat&logoColor=black"></a>
150
  </div>
151
 
152
  Rerun viewer with Gradio
153
+ """,
154
+ elem_classes=["md-custom"],
155
+ header_links=True,
156
+ )
157
  app.render()
158
  gr.Markdown(
159
+ """
160
  ## Installation
161
 
162
  ```bash
 
166
  ## Usage
167
 
168
  ```python
169
+ \"\"\"
170
+ Demonstrates integrating Rerun visualization with Gradio.
171
+
172
+ Provides example implementations of data streaming, keypoint annotation, and dynamic
173
+ visualization across multiple Gradio tabs using Rerun's recording and visualization capabilities.
174
+ \"\"\"
175
+
176
+ import math
177
  import os
178
  import tempfile
179
  import time
180
+ import uuid
181
 
182
+ import cv2
183
  import gradio as gr
 
 
184
  import rerun as rr
185
  import rerun.blueprint as rrb
 
186
  from color_grid import build_color_grid
187
+ from gradio_rerun import Rerun
188
+ from gradio_rerun.events import (
189
+ SelectionChange,
190
+ TimelineChange,
191
+ TimeUpdate,
192
+ )
193
 
194
+
195
+ # Whenever we need a recording, we construct a new recording stream.
196
+ # As long as the app and recording IDs remain the same, the data
197
+ # will be merged by the Viewer.
198
+ def get_recording(recording_id: str) -> rr.RecordingStream:
199
+ return rr.RecordingStream(application_id="rerun_example_gradio", recording_id=recording_id)
200
 
201
 
202
  # A task can directly log to a binary stream, which is routed to the embedded viewer.
 
204
  #
205
  # This is the preferred way to work with Rerun in Gradio since your data can be immediately and
206
  # incrementally seen by the viewer. Also, there are no ephemeral RRDs to cleanup or manage.
207
+ def streaming_repeated_blur(recording_id: str, img):
208
+ # Here we get a recording using the provided recording id.
209
+ rec = get_recording(recording_id)
210
+ stream = rec.binary_stream()
211
 
212
  if img is None:
213
  raise gr.Error("Must provide an image to blur.")
 
220
  collapse_panels=True,
221
  )
222
 
223
+ rec.send_blueprint(blueprint)
224
+ rec.set_time("iteration", sequence=0)
225
+ rec.log("image/original", rr.Image(img))
 
 
226
  yield stream.read()
227
 
228
  blur = img
 
229
  for i in range(100):
230
+ rec.set_time("iteration", sequence=i)
231
 
232
  # Pretend blurring takes a while so we can see streaming in action.
233
  time.sleep(0.1)
234
  blur = cv2.GaussianBlur(blur, (5, 5), 0)
235
+ rec.log("image/blurred", rr.Image(blur))
 
236
 
237
  # Each time we yield bytes from the stream back to Gradio, they
238
  # are incrementally sent to the viewer. Make sure to yield any time
 
240
  yield stream.read()
241
 
242
 
243
+ # In this example the user is able to add keypoints to an image visualized in Rerun.
244
+ # These keypoints are stored in the global state, we use the session id to keep track of which keypoints belong
245
+ # to a specific session (https://www.gradio.app/guides/state-in-blocks).
246
+ #
247
+ # The current session can be obtained by adding a parameter of type `gradio.Request` to your event listener functions.
248
+ Keypoint = tuple[float, float]
249
+ keypoints_per_session_per_sequence_index: dict[str, dict[int, list[Keypoint]]] = {}
250
+
251
+
252
+ def get_keypoints_for_user_at_sequence_index(request: gr.Request, sequence: int) -> list[Keypoint]:
253
+ per_sequence = keypoints_per_session_per_sequence_index[request.session_hash]
254
+ if sequence not in per_sequence:
255
+ per_sequence[sequence] = []
256
+
257
+ return per_sequence[sequence]
258
+
259
+
260
+ def initialize_instance(request: gr.Request) -> None:
261
+ keypoints_per_session_per_sequence_index[request.session_hash] = {}
262
+
263
+
264
+ def cleanup_instance(request: gr.Request) -> None:
265
+ if request.session_hash in keypoints_per_session_per_sequence_index:
266
+ del keypoints_per_session_per_sequence_index[request.session_hash]
267
+
268
+
269
+ # In this function, the `request` and `evt` parameters will be automatically injected by Gradio when this
270
+ # event listener is fired.
271
+ #
272
+ # `SelectionChange` is a subclass of `EventData`: https://www.gradio.app/docs/gradio/eventdata
273
+ # `gr.Request`: https://www.gradio.app/main/docs/gradio/request
274
+ def register_keypoint(
275
+ active_recording_id: str,
276
+ current_timeline: str,
277
+ current_time: float,
278
+ request: gr.Request,
279
+ change: SelectionChange,
280
+ ):
281
+ if active_recording_id == "":
282
+ return
283
+
284
+ if current_timeline != "iteration":
285
+ return
286
+
287
+ evt = change.payload
288
+
289
+ # We can only log a keypoint if the user selected only a single item.
290
+ if len(evt.items) != 1:
291
+ return
292
+ item = evt.items[0]
293
+
294
+ # If the selected item isn't an entity, or we don't have its position, then bail out.
295
+ if item.type != "entity" or item.position is None:
296
+ return
297
+
298
+ # Now we can produce a valid keypoint.
299
+ rec = get_recording(active_recording_id)
300
+ stream = rec.binary_stream()
301
+
302
+ # We round `current_time` toward 0, because that gives us the sequence index
303
+ # that the user is currently looking at, due to the Viewer's latest-at semantics.
304
+ index = math.floor(current_time)
305
+
306
+ # We keep track of the keypoints per sequence index for each user manually.
307
+ keypoints = get_keypoints_for_user_at_sequence_index(request, index)
308
+ keypoints.append(item.position[0:2])
309
+
310
+ rec.set_time("iteration", sequence=index)
311
+ rec.log(f"{item.entity_path}/keypoint", rr.Points2D(keypoints, radii=2))
312
+
313
+ yield stream.read()
314
+
315
+
316
+ def track_current_time(evt: TimeUpdate):
317
+ return evt.payload.time
318
+
319
+
320
+ def track_current_timeline_and_time(evt: TimelineChange):
321
+ return evt.payload.timeline, evt.payload.time
322
+
323
+
324
  # However, if you have a workflow that creates an RRD file instead, you can still send it
325
  # directly to the viewer by simply returning the path to the RRD file.
326
  #
 
328
  # be easily modified to stream data directly via Gradio.
329
  #
330
  # In this case you may want to clean up the RRD file after it's sent to the viewer so that you
331
+ # don't accumulate too many temporary files.
332
  @rr.thread_local_stream("rerun_example_cube_rrd")
333
  def create_cube_rrd(x, y, z, pending_cleanup):
334
  cube = build_color_grid(int(x), int(y), int(z), twist=0)
335
  rr.log("cube", rr.Points3D(cube.positions, colors=cube.colors, radii=0.5))
336
 
337
+ # Simulate delay
338
+ time.sleep(x / 10)
339
+
340
  # We eventually want to clean up the RRD file after it's sent to the viewer, so tracking
341
  # any pending files to be cleaned up when the state is deleted.
342
  temp = tempfile.NamedTemporaryFile(prefix="cube_", suffix=".rrd", delete=False)
 
350
  return temp.name
351
 
352
 
353
+ def cleanup_cube_rrds(pending_cleanup: list[str]) -> None:
354
  for f in pending_cleanup:
355
  os.unlink(f)
356
 
 
361
  img = gr.Image(interactive=True, label="Image")
362
  with gr.Column():
363
  stream_blur = gr.Button("Stream Repeated Blur")
364
+
365
  with gr.Row():
366
  viewer = Rerun(
367
  streaming=True,
 
371
  "selection": "hidden",
372
  },
373
  )
 
374
 
375
+ # We make a new recording id, and store it in a Gradio's session state.
376
+ recording_id = gr.State(uuid.uuid4())
377
+
378
+ # Also store the current timeline and time of the viewer in the session state.
379
+ current_timeline = gr.State("")
380
+ current_time = gr.State(0.0)
381
+
382
+ # When registering the event listeners, we pass the `recording_id` in as input in order to create
383
+ # a recording stream using that id.
384
+ stream_blur.click(
385
+ # Using the `viewer` as an output allows us to stream data to it by yielding bytes from the callback.
386
+ streaming_repeated_blur,
387
+ inputs=[recording_id, img],
388
+ outputs=[viewer],
389
  )
390
+ viewer.selection_change(
391
+ register_keypoint,
392
+ inputs=[recording_id, current_timeline, current_time],
393
+ outputs=[viewer],
394
+ )
395
+ viewer.time_update(track_current_time, outputs=[current_time])
396
+ viewer.timeline_change(track_current_timeline_and_time, outputs=[current_timeline, current_time])
397
+ with gr.Tab("Dynamic RRD"):
398
+ pending_cleanup = gr.State([], time_to_live=10, delete_callback=cleanup_cube_rrds)
399
  with gr.Row():
400
+ x_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="X Count")
401
+ y_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Y Count")
402
+ z_count = gr.Number(minimum=1, maximum=10, value=5, precision=0, label="Z Count")
 
 
 
 
 
 
403
  with gr.Row():
404
  create_rrd = gr.Button("Create RRD")
405
  with gr.Row():
 
439
  },
440
  )
441
  choose_rrd.change(lambda x: x, inputs=[choose_rrd], outputs=[viewer])
442
+ demo.load(initialize_instance)
443
+ demo.close(cleanup_instance)
444
 
445
 
446
  if __name__ == "__main__":
447
  demo.launch()
448
 
449
  ```
450
+ """,
451
+ elem_classes=["md-custom"],
452
+ header_links=True,
453
+ )
454
 
455
+ gr.Markdown(
456
+ """
457
  ## `Rerun`
458
 
459
  ### Initialization
460
+ """,
461
+ elem_classes=["md-custom"],
462
+ header_links=True,
463
+ )
464
 
465
+ gr.ParamViewer(value=_docs["Rerun"]["members"]["__init__"], linkify=["RerunData"])
466
 
467
+ gr.Markdown("### Events")
468
+ gr.ParamViewer(value=_docs["Rerun"]["events"], linkify=["Event"])
469
 
470
+ gr.Markdown(
471
+ """
472
 
473
  ### User function
474
 
 
479
 
480
  The code snippet below is accurate in cases where the component is used as both an input and an output.
481
 
482
+ - **As input:** Is passed, a `RerunData` object.
483
+ - **As output:** Should return, the value to send over to the Rerun viewer on the front-end.
484
 
485
  ```python
486
  def predict(
 
488
  ) -> list[pathlib.Path | str] | pathlib.Path | str | bytes:
489
  return value
490
  ```
491
+ """,
492
+ elem_classes=["md-custom", "Rerun-user-fn"],
493
+ header_links=True,
494
+ )
495
 
496
+ code_RerunData = gr.Markdown(
497
+ """
498
  ## `RerunData`
499
  ```python
500
  class RerunData(GradioRootModel):
501
+ root: Sequence[FileData | Path | str] | None
502
+ ```""",
503
+ elem_classes=["md-custom", "RerunData"],
504
+ header_links=True,
505
+ )
506
 
507
+ demo.load(
508
+ None,
509
+ js=r"""function() {
510
  const refs = {
511
  RerunData: [], };
512
  const user_fn_refs = {
 
541
  })
542
  }
543
 
544
+ """,
545
+ )
546
 
547
  demo.launch()