Skip to content

Commit

Permalink
Expose batch APIs for linestrips (#2822)
Browse files Browse the repository at this point in the history
### What
Although batch linestrips have been supported by our data-model, we
didn't have an API for logging them as such.

This adds two new APIs:
 - `log_line_strips_2d` and `log_line_strips_3d`
 
While being explicit about 2d vs 3d is not consistent with our existing
python API surface, it drastically simplifies the error handling around
type-inference in cases of empty lists, and iterables.

Given our future-looking APIs will also be explicit about APIs, this
departure seems like a reasonable compromise.

Tested with:
```
rr.log_line_strips_3d(
    "simple",
    [
        [
            [0, 0, 2], [1, 0, 2], [1, 1, 2], [0, 1, 2],
        ],
        [
            [0, 0, 0], [0, 0, 1], [1, 0, 0], [1, 0, 1], [1, 1, 0], [1, 1, 1], [0, 1, 0], [0, 1, 1],
        ],
    ],
    identifiers=[17, 42],
    colors=[[255, 0, 0], [0, 255, 0]],
    stroke_widths=[0.05, 0.2],
)
```

![image](https://github.com/rerun-io/rerun/assets/3312232/aa6a2ac5-97cd-4fca-816e-6ceb7b97da07)



### Checklist
* [x] I have read and agree to [Contributor
Guide](https://github.com/rerun-io/rerun/blob/main/CONTRIBUTING.md) and
the [Code of
Conduct](https://github.com/rerun-io/rerun/blob/main/CODE_OF_CONDUCT.md)
* [x] I've included a screenshot or gif (if applicable)
* [x] I have tested [demo.rerun.io](https://demo.rerun.io/pr/2822) (if
applicable)

- [PR Build Summary](https://build.rerun.io/pr/2822)
- [Docs
preview](https://rerun.io/preview/pr%3Ajleibs%2Fbatch_lines/docs)
- [Examples
preview](https://rerun.io/preview/pr%3Ajleibs%2Fbatch_lines/examples)
  • Loading branch information
jleibs authored Jul 26, 2023
1 parent 5b64e66 commit 03fbbb2
Show file tree
Hide file tree
Showing 8 changed files with 306 additions and 8 deletions.
2 changes: 1 addition & 1 deletion crates/re_types/source_hash.txt

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions docs/code-examples/line_strip2d_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Log a batch of 2d line strips."""
import rerun as rr

rr.init("linestrip2d", spawn=True)

rr.log_line_strips_2d(
"batch",
[
[[0, 0], [2, 1], [4, -1], [6, 0]],
[[0, 3], [1, 4], [2, 2], [3, 4], [4, 2], [5, 4], [6, 3]],
],
colors=[[255, 0, 0], [0, 255, 0]],
stroke_widths=[0.05, 0.01],
)

# Log an extra rect to set the view bounds
rr.log_rect("bounds", [3, 1.5, 8, 9], rect_format=rr.RectFormat.XCYCWH)
28 changes: 28 additions & 0 deletions docs/code-examples/line_strip3d_batch.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Log a batch of 3d line strips."""
import rerun as rr

rr.init("linestrip3d", spawn=True)

rr.log_line_strips_3d(
"batch",
[
[
[0, 0, 2],
[1, 0, 2],
[1, 1, 2],
[0, 1, 2],
],
[
[0, 0, 0],
[0, 0, 1],
[1, 0, 0],
[1, 0, 1],
[1, 1, 0],
[1, 1, 1],
[0, 1, 0],
[0, 1, 1],
],
],
colors=[[255, 0, 0], [0, 255, 0]],
stroke_widths=[0.05, 0.01],
)
12 changes: 12 additions & 0 deletions docs/content/reference/data_types/linestrip2d.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,15 @@ code-example: line_segments2d_simple
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/196e0f2fe2222526e9eba87fa39440ada08e273d_line_segment2d_simple_1200w.png">
<img src="https://static.rerun.io/53df596662dd9ffaaea5d09d091ef95220346c83_line_segment2d_simple_full.png" alt="">
</picture>

## Batch Examples

code-example: line_strip2d_batch

<picture>
<source media="(max-width: 480px)" srcset="https://static.rerun.io/25e0ef495714636821fcd4dbf373148016bde195_line_strip2d_batch_480w.png">
<source media="(max-width: 768px)" srcset="https://static.rerun.io/b70bf5eefe036f08ed3e48cf4001cf4deebd86e6_line_strip2d_batch_768w.png">
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/44aaefeb430f8209c41df0c2cd4564538196b99d_line_strip2d_batch_1024w.png">
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/e4cdfae79503362acd773bf1d124e95a1026b356_line_strip2d_batch_1200w.png">
<img src="https://static.rerun.io/d8aae7ca3d6c3b0e3b636de60b8067fa2f0b6db9_line_strip2d_batch_full.png" alt="">
</picture>
12 changes: 12 additions & 0 deletions docs/content/reference/data_types/linestrip3d.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,15 @@ code-example: line_segments3d_simple
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/aeed681be95d6c974446f89a6fa26b7d3077adce_line_segment3d_simple_1200w.png">
<img src="https://static.rerun.io/aa800b2a6e6a7b8e32e762b42861bae36f5014bb_line_segment3d_simple_full.png" alt="">
</picture>

## Batch Examples

code-example: line_strip3d_batch

<picture>
<source media="(max-width: 480px)" srcset="https://static.rerun.io/447c7d3d0a75447aa9bad9cfb2c6d68fbe082935_line_strip3d_batch_480w.png">
<source media="(max-width: 768px)" srcset="https://static.rerun.io/d7a16841654a524521f0d26b81771d4e5a740108_line_strip3d_batch_768w.png">
<source media="(max-width: 1024px)" srcset="https://static.rerun.io/2848a42a715b410f433a9b78ddbe599dea2b66f9_line_strip3d_batch_1024w.png">
<source media="(max-width: 1200px)" srcset="https://static.rerun.io/a5ccbe907ea07baeb5117b17dfde41ce11477bf1_line_strip3d_batch_1200w.png">
<img src="https://static.rerun.io/102e5ec5271475657fbc76b469267e4ec8e84337_line_strip3d_batch_full.png" alt="">
</picture>
4 changes: 3 additions & 1 deletion rerun_py/rerun_sdk/rerun/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@
"log_image_file",
"log_line_segments",
"log_line_strip",
"log_line_strips_2d",
"log_line_strips_3d",
"log_mesh",
"log_mesh_file",
"log_meshes",
Expand Down Expand Up @@ -101,7 +103,7 @@
from .log.extension_components import log_extension_components
from .log.file import ImageFormat, MeshFormat, log_image_file, log_mesh_file
from .log.image import log_depth_image, log_image, log_segmentation_image
from .log.lines import log_line_segments, log_line_strip, log_path
from .log.lines import log_line_segments, log_line_strip, log_line_strips_2d, log_line_strips_3d, log_path
from .log.mesh import log_mesh, log_meshes
from .log.points import log_point, log_points
from .log.rects import RectFormat, log_rect, log_rects
Expand Down
16 changes: 12 additions & 4 deletions rerun_py/rerun_sdk/rerun/components/linestrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,12 @@ def from_numpy_arrays(array: Iterable[npt.NDArray[np.float32]]) -> LineStrip2DAr
for line in array:
assert line.shape[1] == 2

offsets = itertools.chain([0], itertools.accumulate(len(line) for line in array))
values = np.concatenate(array) # type: ignore[call-overload]
offsets = list(itertools.chain([0], itertools.accumulate(len(line) for line in array)))
if len(offsets) > 1:
values = np.concatenate(array) # type: ignore[call-overload]
else:
values = np.array([], dtype=np.float32)

fixed = pa.FixedSizeListArray.from_arrays(values.flatten(), type=LineStrip2DType.storage_type.value_type)
storage = pa.ListArray.from_arrays(offsets, fixed, type=LineStrip2DType.storage_type)

Expand All @@ -46,8 +50,12 @@ def from_numpy_arrays(array: Iterable[npt.NDArray[np.float32]]) -> LineStrip3DAr
for line in array:
assert line.shape[1] == 3

offsets = itertools.chain([0], itertools.accumulate(len(line) for line in array))
values = np.concatenate(array) # type: ignore[call-overload]
offsets = list(itertools.chain([0], itertools.accumulate(len(line) for line in array)))
if len(offsets) > 1:
values = np.concatenate(array) # type: ignore[call-overload]
else:
values = np.array([], dtype=np.float32)

fixed = pa.FixedSizeListArray.from_arrays(values.flatten(), type=LineStrip3DType.storage_type.value_type)
storage = pa.ListArray.from_arrays(offsets, fixed, type=LineStrip3DType.storage_type)

Expand Down
223 changes: 221 additions & 2 deletions rerun_py/rerun_sdk/rerun/log/lines.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from __future__ import annotations

from typing import Any
from typing import Any, Iterable

import numpy as np
import numpy.typing as npt
Expand All @@ -12,14 +12,17 @@
from rerun.components.instance import InstanceArray
from rerun.components.linestrip import LineStrip2DArray, LineStrip3DArray
from rerun.components.radius import RadiusArray
from rerun.log import Color, _normalize_colors, _normalize_radii
from rerun.log import Color, Colors, _normalize_colors, _normalize_radii
from rerun.log.error_utils import _send_warning
from rerun.log.extension_components import _add_extension_components
from rerun.log.log_decorator import log_decorator
from rerun.recording_stream import RecordingStream

__all__ = [
"log_path",
"log_line_strip",
"log_line_strips_2d",
"log_line_strips_3d",
"log_line_segments",
]

Expand Down Expand Up @@ -129,6 +132,222 @@ def log_line_strip(
bindings.log_arrow_msg(entity_path, components=instanced, timeless=timeless, recording=recording)


@log_decorator
def log_line_strips_2d(
entity_path: str,
line_strips: Iterable[npt.ArrayLike] | None,
*,
identifiers: npt.ArrayLike | None = None,
stroke_widths: npt.ArrayLike | None = None,
colors: Color | Colors | None = None,
draw_order: float | None = None,
ext: dict[str, Any] | None = None,
timeless: bool = False,
recording: RecordingStream | None = None,
) -> None:
r"""
Log a batch of line strips through 2D space.
Each line strip is a list of points connected by line segments. It can be used to draw
approximations of smooth curves.
The points will be connected in order, like so:
```
2------3 5
/ \ /
0----1 \ /
4
```
Parameters
----------
entity_path:
Path to the path in the space hierarchy
line_strips:
An iterable of Nx2 arrays of points along the path.
To log an empty line_strip use `np.zeros((0,0,3))` or `np.zeros((0,0,2))`
identifiers:
Unique numeric id that shows up when you hover or select the line.
stroke_widths:
Optional widths of the line.
colors:
Optional colors of the lines.
RGB or RGBA in sRGB gamma-space as either 0-1 floats or 0-255 integers, with separate alpha.
draw_order:
An optional floating point value that specifies the 2D drawing order.
Objects with higher values are drawn on top of those with lower values.
The default for lines is 20.0.
ext:
Optional dictionary of extension components. See [rerun.log_extension_components][]
timeless:
If true, the path will be timeless (default: False).
recording:
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
See also: [`rerun.init`][], [`rerun.set_global_data_recording`][].
"""
recording = RecordingStream.to_native(recording)

colors = _normalize_colors(colors)
stroke_widths = _normalize_radii(stroke_widths)
radii = stroke_widths / 2.0

identifiers_np = np.array((), dtype="uint64")
if identifiers is not None:
try:
identifiers_np = np.require(identifiers, dtype="uint64")
except ValueError:
_send_warning("Only integer identifiers supported", 1)

# 0 = instanced, 1 = splat
comps = [{}, {}] # type: ignore[var-annotated]

if line_strips is not None:
line_strip_arrs = [np.require(line, dtype="float32") for line in line_strips]
dims = [line.shape[1] for line in line_strip_arrs]

if any(d != 2 for d in dims):
raise ValueError("All line strips must be Nx2")

comps[0]["rerun.linestrip2d"] = LineStrip2DArray.from_numpy_arrays(line_strip_arrs)

if len(identifiers_np):
comps[0]["rerun.instance_key"] = InstanceArray.from_numpy(identifiers_np)

if len(colors):
is_splat = len(colors.shape) == 1
if is_splat:
colors = colors.reshape(1, len(colors))
comps[is_splat]["rerun.colorrgba"] = ColorRGBAArray.from_numpy(colors)

# We store the stroke_width in radius
if len(radii):
is_splat = len(radii) == 1
comps[is_splat]["rerun.radius"] = RadiusArray.from_numpy(radii)

if draw_order is not None:
comps[1]["rerun.draw_order"] = DrawOrderArray.splat(draw_order)

if ext:
_add_extension_components(comps[0], comps[1], ext, identifiers_np)

if comps[1]:
comps[1]["rerun.instance_key"] = InstanceArray.splat()
bindings.log_arrow_msg(entity_path, components=comps[1], timeless=timeless, recording=recording)

# Always the primary component last so range-based queries will include the other data. See(#1215)
bindings.log_arrow_msg(entity_path, components=comps[0], timeless=timeless, recording=recording)


@log_decorator
def log_line_strips_3d(
entity_path: str,
line_strips: Iterable[npt.ArrayLike] | None,
*,
identifiers: npt.ArrayLike | None = None,
stroke_widths: npt.ArrayLike | None = None,
colors: Color | Colors | None = None,
draw_order: float | None = None,
ext: dict[str, Any] | None = None,
timeless: bool = False,
recording: RecordingStream | None = None,
) -> None:
r"""
Log a batch of line strips through 3D space.
Each line strip is a list of points connected by line segments. It can be used to draw approximations
of smooth curves.
The points will be connected in order, like so:
```
2------3 5
/ \ /
0----1 \ /
4
```
Parameters
----------
entity_path:
Path to the path in the space hierarchy
line_strips:
An iterable of Nx3 arrays of points along the path.
To log an empty line_strip use `np.zeros((0,0,3))` or `np.zeros((0,0,2))`
identifiers:
Unique numeric id that shows up when you hover or select the line.
stroke_widths:
Optional widths of the line.
colors:
Optional colors of the lines.
RGB or RGBA in sRGB gamma-space as either 0-1 floats or 0-255 integers, with separate alpha.
draw_order:
An optional floating point value that specifies the 2D drawing order.
Objects with higher values are drawn on top of those with lower values.
The default for lines is 20.0.
ext:
Optional dictionary of extension components. See [rerun.log_extension_components][]
timeless:
If true, the path will be timeless (default: False).
recording:
Specifies the [`rerun.RecordingStream`][] to use.
If left unspecified, defaults to the current active data recording, if there is one.
See also: [`rerun.init`][], [`rerun.set_global_data_recording`][].
"""
recording = RecordingStream.to_native(recording)

colors = _normalize_colors(colors)
stroke_widths = _normalize_radii(stroke_widths)
radii = stroke_widths / 2.0

identifiers_np = np.array((), dtype="uint64")
if identifiers is not None:
try:
identifiers_np = np.require(identifiers, dtype="uint64")
except ValueError:
_send_warning("Only integer identifiers supported", 1)

# 0 = instanced, 1 = splat
comps = [{}, {}] # type: ignore[var-annotated]

if line_strips is not None:
line_strip_arrs = [np.require(line, dtype="float32") for line in line_strips]
dims = [line.shape[1] for line in line_strip_arrs]

if any(d != 3 for d in dims):
raise ValueError("All line strips must be Nx3")

comps[0]["rerun.linestrip3d"] = LineStrip3DArray.from_numpy_arrays(line_strip_arrs)

if len(identifiers_np):
comps[0]["rerun.instance_key"] = InstanceArray.from_numpy(identifiers_np)

if len(colors):
is_splat = len(colors.shape) == 1
if is_splat:
colors = colors.reshape(1, len(colors))
comps[is_splat]["rerun.colorrgba"] = ColorRGBAArray.from_numpy(colors)

# We store the stroke_width in radius
if len(radii):
is_splat = len(radii) == 1
comps[is_splat]["rerun.radius"] = RadiusArray.from_numpy(radii)

if draw_order is not None:
comps[1]["rerun.draw_order"] = DrawOrderArray.splat(draw_order)

if ext:
_add_extension_components(comps[0], comps[1], ext, identifiers_np)

if comps[1]:
comps[1]["rerun.instance_key"] = InstanceArray.splat()
bindings.log_arrow_msg(entity_path, components=comps[1], timeless=timeless, recording=recording)

# Always the primary component last so range-based queries will include the other data. See(#1215)
bindings.log_arrow_msg(entity_path, components=comps[0], timeless=timeless, recording=recording)


@log_decorator
def log_line_segments(
entity_path: str,
Expand Down

0 comments on commit 03fbbb2

Please sign in to comment.