-
Notifications
You must be signed in to change notification settings - Fork 453
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
Better Camera unproject #2001
Better Camera unproject #2001
Changes from 10 commits
f10aaed
21c90f4
7140c54
ce77520
70432ba
476261a
71c31b1
c3c66b0
fd23b2d
586072c
d455cd2
189a9f6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
"default_agent": 0, | ||
"sensor_height": 1.5, | ||
"hfov": 90, | ||
"zfar": 1000.0, | ||
"color_sensor": True, | ||
"semantic_sensor": False, | ||
"depth_sensor": False, | ||
|
@@ -30,6 +31,7 @@ | |
"equirect_semantic_sensor": False, | ||
"seed": 1, | ||
"physics_config_file": "data/default.physics_config.json", | ||
"enable_physics": True, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What are the consequences of enabling physics by default here? Do we want to do this? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fair point. This means any Simulator initialized from the default settings util will have access to Bullet API (if installed). Some justification:
We could add the breaking change tag to the PR and mention this in the description. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sounds good. I think we can proceed with it here. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think that it would be preferable to add this change to another PR for traceability. However, I don't feel strongly either way. |
||
} | ||
# [/default_sim_settings] | ||
|
||
|
@@ -79,6 +81,7 @@ def create_camera_spec(**kw_args): | |
color_sensor_spec = create_camera_spec( | ||
uuid="color_sensor", | ||
hfov=settings["hfov"], | ||
far=settings["zfar"], | ||
sensor_type=habitat_sim.SensorType.COLOR, | ||
sensor_subtype=habitat_sim.SensorSubType.PINHOLE, | ||
) | ||
|
@@ -88,6 +91,7 @@ def create_camera_spec(**kw_args): | |
depth_sensor_spec = create_camera_spec( | ||
uuid="depth_sensor", | ||
hfov=settings["hfov"], | ||
far=settings["zfar"], | ||
sensor_type=habitat_sim.SensorType.DEPTH, | ||
channels=1, | ||
sensor_subtype=habitat_sim.SensorSubType.PINHOLE, | ||
|
@@ -98,6 +102,7 @@ def create_camera_spec(**kw_args): | |
semantic_sensor_spec = create_camera_spec( | ||
uuid="semantic_sensor", | ||
hfov=settings["hfov"], | ||
far=settings["zfar"], | ||
sensor_type=habitat_sim.SensorType.SEMANTIC, | ||
channels=1, | ||
sensor_subtype=habitat_sim.SensorSubType.PINHOLE, | ||
|
@@ -107,6 +112,7 @@ def create_camera_spec(**kw_args): | |
if settings["ortho_rgba_sensor"]: | ||
ortho_rgba_sensor_spec = create_camera_spec( | ||
uuid="ortho_rgba_sensor", | ||
far=settings["zfar"], | ||
sensor_type=habitat_sim.SensorType.COLOR, | ||
sensor_subtype=habitat_sim.SensorSubType.ORTHOGRAPHIC, | ||
) | ||
|
@@ -115,6 +121,7 @@ def create_camera_spec(**kw_args): | |
if settings["ortho_depth_sensor"]: | ||
ortho_depth_sensor_spec = create_camera_spec( | ||
uuid="ortho_depth_sensor", | ||
far=settings["zfar"], | ||
sensor_type=habitat_sim.SensorType.DEPTH, | ||
channels=1, | ||
sensor_subtype=habitat_sim.SensorSubType.ORTHOGRAPHIC, | ||
|
@@ -124,6 +131,7 @@ def create_camera_spec(**kw_args): | |
if settings["ortho_semantic_sensor"]: | ||
ortho_semantic_sensor_spec = create_camera_spec( | ||
uuid="ortho_semantic_sensor", | ||
far=settings["zfar"], | ||
sensor_type=habitat_sim.SensorType.SEMANTIC, | ||
channels=1, | ||
sensor_subtype=habitat_sim.SensorSubType.ORTHOGRAPHIC, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,8 @@ | |
# This source code is licensed under the MIT license found in the | ||
# LICENSE file in the root directory of this source tree. | ||
|
||
import random | ||
import typing | ||
from os import path as osp | ||
|
||
import magnum as mn | ||
|
@@ -19,13 +21,21 @@ | |
not osp.exists("data/scene_datasets/habitat-test-scenes/skokloster-castle.glb"), | ||
reason="Requires the habitat-test-scenes", | ||
) | ||
def test_unproject(): | ||
@pytest.mark.skipif( | ||
not habitat_sim.built_with_bullet, | ||
reason="Bullet physics used for validation.", | ||
) | ||
@pytest.mark.parametrize("zfar", [500, 1000, 1500]) | ||
def test_unproject(zfar): | ||
cfg_settings = habitat_sim.utils.settings.default_sim_settings.copy() | ||
|
||
# configure some settings in case defaults change | ||
cfg_settings["scene"] = "data/scene_datasets/habitat-test-scenes/apartment_1.glb" | ||
cfg_settings["width"] = 101 | ||
cfg_settings["height"] = 101 | ||
cfg_settings["enable_physics"] = True | ||
cfg_settings["depth_sensor"] = True | ||
cfg_settings["zfar"] = zfar | ||
cfg_settings["width"] = 501 | ||
cfg_settings["height"] = 501 | ||
cfg_settings["sensor_height"] = 0 | ||
cfg_settings["color_sensor"] = True | ||
|
||
|
@@ -37,22 +47,101 @@ def test_unproject(): | |
sim.agents[0].scene_node.translation = mn.Vector3(0.5, 0, 0) | ||
|
||
# setup camera | ||
far_plane = sim._sensors["depth_sensor"]._sensor_object.far_plane_dist | ||
assert zfar == far_plane | ||
render_camera = sim._sensors["color_sensor"]._sensor_object.render_camera | ||
depth_camera = sim._sensors["depth_sensor"]._sensor_object.render_camera | ||
|
||
# test unproject | ||
# test unproject with known values | ||
center_ray = render_camera.unproject( | ||
mn.Vector2i(50, 50) | ||
mn.Vector2i(250, 250), normalized=False | ||
) # middle of the viewport | ||
center_ray_normalized = render_camera.unproject(mn.Vector2i(250, 250)) | ||
assert np.allclose( | ||
center_ray_normalized.direction, | ||
center_ray.direction.normalized(), | ||
atol=0.07, | ||
) | ||
assert np.allclose(center_ray.origin, np.array([0.5, 0, 0]), atol=0.07) | ||
assert np.allclose(center_ray.direction, np.array([0, 0, -1.0]), atol=0.02) | ||
assert np.allclose( | ||
center_ray_normalized.direction, np.array([0, 0, -1.0]), atol=0.02 | ||
) | ||
|
||
# NOTE: viewport y==0 is at the top | ||
test_ray_2 = render_camera.unproject( | ||
mn.Vector2i(100, 100) | ||
mn.Vector2i(500, 500), normalized=False | ||
) # bottom right of the viewport | ||
test_ray_2_normalized = render_camera.unproject(mn.Vector2i(500, 500)) | ||
assert np.allclose( | ||
test_ray_2_normalized.direction, | ||
test_ray_2.direction.normalized(), | ||
atol=0.07, | ||
) | ||
assert np.allclose( | ||
test_ray_2.direction, np.array([0.569653, -0.581161, -0.581161]), atol=0.07 | ||
test_ray_2_normalized.direction, | ||
np.array([0.569653, -0.581161, -0.581161]), | ||
atol=0.07, | ||
) | ||
|
||
# add a primitive sphere object to the world | ||
obj_template_mgr = sim.get_object_template_manager() | ||
rigid_obj_mgr = sim.get_rigid_object_manager() | ||
sphere_prim_handle = obj_template_mgr.get_template_handles("uvSphereSolid")[0] | ||
sphere_template = obj_template_mgr.get_template_by_handle(sphere_prim_handle) | ||
sphere_template.scale = [0.03, 0.03, 0.03] | ||
obj_template_mgr.register_template(sphere_template, "scaled_sphere") | ||
sphere_prim_handle = obj_template_mgr.get_template_handles("scaled_sphere")[0] | ||
sphere_obj = rigid_obj_mgr.add_object_by_template_handle(sphere_prim_handle) | ||
|
||
# validate that random unprojected points scaled by depth camera distance are actually on the render mesh | ||
# do this by creating a small collision object at the unprojected point and test it against scene geometry | ||
num_samples = 10 | ||
# move the camera, test a random pixel | ||
cur_sample = 0 | ||
while cur_sample < num_samples: | ||
# move agent | ||
sim.agents[0].scene_node.translation = np.random.random(3) | ||
# rotate agent | ||
sim.agents[0].scene_node.rotation = mn.Quaternion.rotation( | ||
mn.Rad(random.random() * mn.math.tau), mn.Vector3(0, 1, 0) | ||
) | ||
# tilt the camera | ||
render_camera.node.rotation = mn.Quaternion.rotation( | ||
mn.Rad(random.random()), mn.Vector3(1, 0, 0) | ||
) | ||
depth_camera.node.rotation = render_camera.node.rotation | ||
|
||
# do the unprojection from depth image | ||
view_point = mn.Vector2i( | ||
random.randint(0, render_camera.viewport[0] - 1), | ||
random.randint(0, render_camera.viewport[1] - 1), | ||
) | ||
# NOTE: use un-normlized rays scaled to unit z distance for this application | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Typo: normlized |
||
ray = render_camera.unproject(view_point, normalized=False) | ||
depth_obs = typing.cast( | ||
np.ndarray, sim.get_sensor_observations()["depth_sensor"] | ||
) | ||
# NOTE: (height, width) for buffer access | ||
depth = depth_obs[view_point[1]][view_point[0]] | ||
|
||
if depth == 0.0: | ||
# skip depths of 0 which represent empty/background pixels | ||
continue | ||
|
||
# update the collision test object | ||
depth_point = ray.origin + ray.direction * depth | ||
sphere_obj.translation = depth_point | ||
|
||
# optionally render the frames for debugging | ||
# import habitat_sim.utils.viz_utils as vut | ||
# c_image = vut.observation_to_image(sim.get_sensor_observations()["color_sensor"], "color") | ||
# c_image.show() | ||
|
||
assert ( | ||
sphere_obj.contact_test() | ||
), "Must be intersecting with scene collision mesh." | ||
cur_sample += 1 | ||
|
||
|
||
@pytest.mark.parametrize( | ||
"sensor_type", | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Again ignore if this utility isn't meant to be used in performance-sensitive scenarios.)
A better way would be to have this split into two functions, one
unproject()
that doesn't normalize, and anunprojectNormalized()
that callsunproject()
, normalizes its result and returns it. That way there's no branch. (Though that way it'll be a breaking change, becauseunproject()
will not normalize anymore, so you might want to invent different naming of the two overloads.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, thanks. I thought about this also, but didn't want a breaking change or to add more bulk to the API. I think this one can stay for now.