Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Visible/Non Visible Specifier Changes #214

Merged
merged 33 commits into from
Apr 19, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
06f1335
Visibility specifier changes to no longer underapproximate.
Eric-Vin Feb 2, 2024
dc30c08
Updated visible specifiers docs.
Eric-Vin Feb 2, 2024
642f651
Fixes and pitch tweaks.
Eric-Vin Feb 2, 2024
1c1b49b
Merge branch 'main' into VisibleSpecifierChanges
Eric-Vin Feb 6, 2024
6cdc455
Fixes and test tweaks for new semantics.
Eric-Vin Feb 7, 2024
6c6d72f
Fixed checkCyclical.
Eric-Vin Feb 16, 2024
d646b69
Progress on pruning tweaks.
Eric-Vin Feb 17, 2024
6cfef98
Visiblity pruning progress.
Eric-Vin Feb 22, 2024
4ff8ab7
Work on Voxel to Mesh conversion.
Eric-Vin Feb 28, 2024
9ebfe93
More visible spec changes.
Eric-Vin Feb 28, 2024
60e3dac
Voxel to Mesh Workaround
Eric-Vin Mar 15, 2024
801f179
Documentation edits.
Eric-Vin Mar 18, 2024
1002c47
Update src/scenic/core/regions.py
Eric-Vin Mar 18, 2024
af6e5e7
Merge branch 'VisibleSpecifierChanges' of github.com:BerkeleyLearnVer…
Eric-Vin Mar 18, 2024
b499299
Test cleanup
Eric-Vin Mar 18, 2024
e7aea48
Test tweaks.
Eric-Vin Mar 18, 2024
f6288f8
Add WIP warnings to VoxelRegion.
Eric-Vin Mar 18, 2024
075c458
Merge branch 'main' into VisibleSpecifierChanges
Eric-Vin Mar 18, 2024
b3905ff
Added test for visibility pruning with offset.
Eric-Vin Mar 25, 2024
66ef021
Ensure test to ensure offset is preserved after visibility pruning.
Eric-Vin Mar 25, 2024
d54e9b7
Added validation tests.
Eric-Vin Mar 25, 2024
d9c27fa
Added test for intersection region sampler.
Eric-Vin Mar 26, 2024
2f3e359
Test modification for extra coverage.
Eric-Vin Mar 26, 2024
074364c
Generic sampling tests/fixes.
Eric-Vin Mar 26, 2024
11d17e1
Various requested fixes.
Eric-Vin Apr 16, 2024
341f6be
Apply suggestions from code review
Eric-Vin Apr 16, 2024
37f6f87
Docs updates.
Eric-Vin Apr 16, 2024
fd26356
Merge branch 'VisibleSpecifierChanges' of github.com:BerkeleyLearnVer…
Eric-Vin Apr 16, 2024
8c58c25
Update tests/syntax/test_specifiers.py
Eric-Vin Apr 18, 2024
df2aa8a
Visibility test cleanup
Eric-Vin Apr 18, 2024
e6c3fdb
Voxel mesh cleanup and test.
Eric-Vin Apr 18, 2024
5b870ed
Voxel to mesh cleanup.
Eric-Vin Apr 18, 2024
35fa757
Modified pitch_signs formatting.
Eric-Vin Apr 19, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/porting.rst
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,7 @@ Specifically:

* The specifier :specifier:`with heading {X}` is replaced with :specifier:`facing {X}`.

* The :specifier:`visible` and :specifier:`not visible` will behave as they did in Scenic 2. Specifically, :specifier:`visible` will specify position from the observing object's visible region and :specifier:`not visible` will specify position from the difference of the workspace and the observing object's visible region.
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved

Note that despite these changes, Scenic will still use 3D geometry internally.
For example, if you write :scenic:`ego = new Object at (1, 2)` the value of :scenic:`ego.position` will be the 3D vector :scenic:`(1, 2, 0)`.
15 changes: 11 additions & 4 deletions docs/reference/specifiers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -192,8 +192,11 @@ visible [from (*Point* | *OrientedPoint*)]

Requires that this object is visible from the :scenic:`ego` or the given `Point`/`OrientedPoint`. See the :ref:`Visibility System <visibility>` reference for a discussion of the visibility model.

Also optionally specifies :prop:`position` to be a uniformly random point in the :term:`visible region` of the ego, or of the given Point/OrientedPoint if given.
Note that the position set by this specifier is slightly stricter than simply adding a requirement that the ego :keyword:`can see` the object: the specifier makes the *center* of the object (its :prop:`position`) visible, while the :keyword:`can see` condition will be satisfied even if the center is not visible as long as some other part of the object is visible.
Also optionally specifies :prop:`position` to be uniformly random over all points that could result in a visible object (note that the above requirement will ensure the object is in fact visible).

.. versionchanged:: 3.0

This specifier now specifies position uniformly randomly over all points that could result in a visible object. This accounts for objects who's position might be out of the visible region, but who have a portion of their occupied space visible (e.g. a corner that is visible), which would never be generated with the previous semantics.
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved

.. _not visible [from ({Point} | {OrientedPoint})]:

Expand All @@ -209,8 +212,12 @@ not visible [from (*Point* | *OrientedPoint*)]

Requires that this object is *not* visible from the ego or the given `Point`/`OrientedPoint`.

Similarly to :sampref:`visible [from ({Point} | {OrientedPoint})]`, this specifier can position the object uniformly at random in the *non-visible* region of the ego.
However, it depends on :prop:`regionContainedIn`, in order to restrict the non-visible region to the :term:`container` of the object being created, which is hopefully a bounded region (if the non-visible region is unbounded, it cannot be uniformly sampled from and an error will be raised).
Similarly to :sampref:`visible [from ({Point} | {OrientedPoint})]`, this specifier can optionally position the object uniformly at random over all points that could result in a non-visible object (note that the above requirement will ensure the object is in fact not-visible).
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved

.. versionchanged:: 3.0

This specifier now specifies position uniformly randomly over all points that could result in a non-visible object. This accounts for objects who's position might be out of the visible region, but who have a portion of their occupied space visible (e.g. a corner that is visible), which could be generated with the previous semantics (and now will not).
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved


.. _(left | right) of {vector} [by {scalar}]:
.. _left of:
Expand Down
4 changes: 2 additions & 2 deletions docs/syntax_guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -219,9 +219,9 @@ Additional specifiers for the :prop:`position` and :prop:`orientation` propertie
* - :sampref:`beyond {vector} by ({vector} | {scalar}) [from ({vector} | {OrientedPoint})]`
- Positions the object with respect to the line of sight from a point or the ego
* - :sampref:`visible [from ({Point} | {OrientedPoint})]`
- Ensures the object is visible from the ego, or from the given Point/OrientedPoint if given, while optionally specifying position to be in the appropriate visible region.
- Ensures the object is visible from the ego, or from the given Point/OrientedPoint if given, while optionally specifying position to be uniformly random over all positions that result in a visible object.
* - :sampref:`not visible [from ({Point} | {OrientedPoint})]`
- Ensures the object is not visible from the ego, or from the given Point/OrientedPoint if given, while optionally specifying position to be outside the appropriate visible region.
- Ensures the object is not visible from the ego, or from the given Point/OrientedPoint if given, while optionally specifying position to be uniformly random over all positions that result in a non-visible object.
* - :sampref:`(left | right) of ({vector} | {OrientedPoint} | {Object}) [by {scalar}] <left of>`
- Positions the object to the left/right by the given scalar distance.
* - :sampref:`(ahead of | behind) ({vector} | {OrientedPoint} | {Object}) [by {scalar}] <ahead of>`
Expand Down
45 changes: 7 additions & 38 deletions src/scenic/core/pruning.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,31 +237,14 @@ def pruneContainment(scenario, verbosity):
container = container.buffer(-maxErosion)
elif isinstance(container, MeshVolumeRegion):
# We can attempt to erode a voxel approximation of the MeshVolumeRegion.
# Compute a voxel overapproximation of the mesh. Technically this is not
# an overapproximation, but one dilation with a rank 3 structuring unit
# with connectivity 3 is. To simplify, we just erode one less time than
# needed.
target_pitch = PRUNING_PITCH * max(container.mesh.extents)
voxelized_container = container.voxelized(target_pitch, lazy=True)

# Erode the voxel region. Erosion is done with a rank 3 structuring unit with
# connectivity 3 (a 3x3x3 cube of voxels). Each erosion pass can erode by at
# most math.hypot([pitch]*3). Therefore we can safely make at most
# floor(maxErosion/math.hypot([pitch]*3)) passes without eroding more
# than maxErosion. We also subtract 1 iteration for the reasons above.
iterations = (
math.floor(maxErosion / math.hypot(*([target_pitch] * 3))) - 1
eroded_container = container._erodeOverapproximate(
maxErosion, PRUNING_PITCH
)

if iterations > 0:
eroded_container = voxelized_container.dilation(
iterations=-iterations
)

# Now check if this erosion is useful, i.e. do we have less volume to sample from.
# If so, replace the original container.
if eroded_container.size < container.size:
container = eroded_container
# Now check if this erosion is useful, i.e. do we have less volume to sample from.
# If so, replace the original container.
if eroded_container.size < container.size:
container = eroded_container

# Restrict the base region to the possibly eroded container, unless
# they're the same in which case we're done
Expand Down Expand Up @@ -408,21 +391,7 @@ def pruneVisibility(scenario, verbosity):
# in a region that contains all points that could feasibly be the position
# of obj, if it is visible from the observer.
def bufferHelper(viewRegion):
# Compute a voxel overapproximation of the mesh. Technically this is not
# an overapproximation, but one dilation with a rank 3 structuring unit
# with connectivity 3 is. To simplify, we just dilate one additional time.
target_pitch = PRUNING_PITCH * max(viewRegion.mesh.extents)
voxelized_vr = viewRegion.voxelized(target_pitch, lazy=True)

# Dilate the voxel region. Dilation is done with a rank 3 structuring unit with
# connectivity 3 (a 3x3x3 cube of voxels). Each dilation pass must dilate by at
# least pitch. Therefore we must make at least ceiling((radius/2)/pitch) passes
# to ensure we have dilated by the half the circumradius of the object. We also
# add 1 iteration for the reasons above.
iterations = math.ceil((obj.radius / 2) / target_pitch) + 1
dilated_vr = voxelized_vr.dilation(iterations=iterations)

return dilated_vr
return viewRegion._bufferOverapproximate(obj.radius / 2, PRUNING_PITCH)

# Prune based off visibility/non-visibility requirements
if obj.requireVisible:
Expand Down
55 changes: 55 additions & 0 deletions src/scenic/core/regions.py
Original file line number Diff line number Diff line change
Expand Up @@ -1729,6 +1729,11 @@ def inradius(self):
else:
return region_distance

@cached_property
@distributionFunction
def circumradius(self):
hypot(*(self.mesh.extents / 2))
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved

@property
def dimensionality(self):
return 3
Expand All @@ -1742,6 +1747,56 @@ def voxelized(self, pitch, lazy=False):
"""Returns a VoxelRegion representing a filled voxelization of this mesh"""
return VoxelRegion(voxelGrid=self.mesh.voxelized(pitch).fill(), lazy=lazy)

@distributionFunction
def _erodeOverapproximate(self, maxErosion, pitch):
"""Compute an overapproximation of this region eroded.

Erode as much as possible, but no more than maxErosion, outputting
a VoxelRegion. Note that this can sometimes return a larger region
than the original mesh
"""
# Compute a voxel overapproximation of the mesh. Technically this is not
# an overapproximation, but one dilation with a rank 3 structuring unit
# with connectivity 3 is. To simplify, we just erode one less time than
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved
# needed.
target_pitch = pitch * max(self.mesh.extents)
voxelized_mesh = self.voxelized(target_pitch, lazy=True)

# Erode the voxel region. Erosion is done with a rank 3 structuring unit with
# connectivity 3 (a 3x3x3 cube of voxels). Each erosion pass can erode by at
# most math.hypot([pitch]*3). Therefore we can safely make at most
# floor(maxErosion/math.hypot([pitch]*3)) passes without eroding more
# than maxErosion. We also subtract 1 iteration for the reasons above.
iterations = math.floor(maxErosion / math.hypot(*([target_pitch] * 3))) - 1

eroded_mesh = voxelized_mesh.dilation(iterations=-iterations)

return eroded_mesh

@distributionFunction
def _bufferOverapproximate(self, minBuffer, pitch):
"""Compute an overapproximation of this region buffered.

Buffer as little as possible, but at least minBuffer, outputting
a VoxelRegion.
"""
# Compute a voxel overapproximation of the mesh. Technically this is not
# an overapproximation, but one dilation with a rank 3 structuring unit
# with connectivity 3 is. To simplify, we just dilate one additional time
# than needed.
target_pitch = pitch * max(self.mesh.extents)
voxelized_mesh = self.voxelized(target_pitch, lazy=True)

# Dilate the voxel region. Dilation is done with a rank 3 structuring unit with
# connectivity 3 (a 3x3x3 cube of voxels). Each dilation pass must dilate by at
# least pitch. Therefore we must make at least ceil(minBuffer/pitch) passes to
# guarantee dilating at least minBuffer. We also add 1 iteration for the reasons above.
iterations = math.ceil(minBuffer / pitch) + 1

dilated_mesh = voxelized_mesh.dilation(iterations=iterations)

return dilated_mesh

@cached_method
def getSurfaceRegion(self):
"""Return a region equivalent to this one, except as a MeshSurfaceRegion"""
Expand Down
27 changes: 23 additions & 4 deletions src/scenic/syntax/veneer.py
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,7 @@
evaluatingGuard = False
mode2D = False
_originalConstructibles = (Point, OrientedPoint, Object)
BUFFERING_PITCH = 0.1

## APIs used internally by the rest of Scenic

Expand Down Expand Up @@ -1612,10 +1613,29 @@ def VisibleFrom(base):
if not isA(base, Point):
raise TypeError('specifier "visible from O" with O not a Point')

def helper(self):
if mode2D:
position = Region.uniformPointIn(base.visibleRegion)
else:
# We can limit the potential positions to an overapproximation of the
# visible region.
hw = self.width / 2
hl = self.length / 2
hh = self.height / 2
radius = hypot(hw, hl, hh)

buffered_vr = base.visibleRegion._bufferOverapproximate(
radius / 2, BUFFERING_PITCH
)

position = Region.uniformPointIn(buffered_vr)

return {"position": position, "_observingEntity": base}

return Specifier(
"Visible/VisibleFrom",
{"position": 3, "_observingEntity": 1},
{"position": Region.uniformPointIn(base.visibleRegion), "_observingEntity": base},
DelayedArgument({"width", "length", "height"}, helper),
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved
)


Expand Down Expand Up @@ -1649,9 +1669,8 @@ def helper(self):
if mode2D:
position = Region.uniformPointIn(region.difference(base.visibleRegion))
else:
position = Region.uniformPointIn(
convertToFootprint(region).difference(base.visibleRegion)
)
# We can't limit the available region since any spot could potentially be occluded.
position = Region.uniformPointIn(convertToFootprint(region))

return {"position": position, "_nonObservingEntity": base}

Expand Down
20 changes: 12 additions & 8 deletions tests/syntax/test_specifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,9 @@ def test_visible():
for i in range(30):
scene = sampleScene(scenario, maxIterations=10)
ego, base = scene.objects
assert ego.position.distanceTo(base.position) <= 10
assert ego.position.x >= base.position.x
assert ego.position.y >= base.position.y
assert ego.position.distanceTo(base.position) <= 10 + math.sqrt(3 * 0.5**2)
assert ego.position.x >= base.position.x - math.sqrt(3 * 0.5**2)
assert ego.position.y >= base.position.y - math.sqrt(3 * 0.5**2)
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved


def test_visible_no_ego():
Expand All @@ -525,7 +525,9 @@ def test_visible_from_point():
)
for i in range(20):
scene = sampleScene(scenario, maxIterations=10)
assert scene.egoObject.position.distanceTo(Vector(300, 200)) <= 2
assert scene.egoObject.position.distanceTo(Vector(300, 200)) <= 2 + math.sqrt(
3 * 0.5**2
Eric-Vin marked this conversation as resolved.
Show resolved Hide resolved
)


def test_visible_from_point_3d():
Expand All @@ -535,7 +537,9 @@ def test_visible_from_point_3d():
)
for i in range(20):
scene = sampleScene(scenario, maxIterations=10)
assert scene.egoObject.position.distanceTo(Vector(300, 200, 500)) <= 2
assert scene.egoObject.position.distanceTo(
Vector(300, 200, 500)
) <= 2 + math.sqrt(3 * 0.5**2)


def test_visible_from_oriented_point():
Expand All @@ -548,9 +552,9 @@ def test_visible_from_oriented_point():
for i in range(20):
scene = sampleScene(scenario, maxIterations=10)
pos = scene.egoObject.position
assert pos.distanceTo(base) <= 5
assert pos.x <= base.x
assert pos.y >= base.y
assert pos.distanceTo(base) <= 5 + math.sqrt(3 * 0.5**2)
assert pos.x <= base.x + math.sqrt(3 * 0.5**2)
assert pos.y >= base.y - math.sqrt(3 * 0.5**2)


@pytest.mark.slow
Expand Down
Loading