Commit 15c72be4 authored by Nikhila Ravi's avatar Nikhila Ravi Committed by Facebook Github Bot
Browse files

Fix coordinate system conventions in renderer

Summary:
## Updates

- Defined the world and camera coordinates according to this figure. The world coordinates are defined as having +Y up, +X left and +Z in.

{F230888499}

- Removed all flipping from blending functions.
- Updated the rasterizer to return images with +Y up and +X left.
- Updated all the mesh rasterizer tests
    - The expected values are now defined in terms of the default +Y up, +X left
    - Added tests where the triangles in the meshes are non symmetrical so that it is clear which direction +X and +Y are

## Questions:
- Should we have **scene settings** instead of raster settings?
    - To be more correct we should be [z clipping in the rasterizer based on the far/near clipping planes](https://github.com/ShichenLiu/SoftRas/blob/master/soft_renderer/cuda/soft_rasterize_cuda_kernel.cu#L400) - these values are also required in the blending functions so should we make these scene level parameters and have a scene settings tuple which is available to the rasterizer and shader?

Reviewed By: gkioxari

Differential Revision: D20208604

fbshipit-source-id: 55787301b1bffa0afa9618f0a0886cc681da51f3
parent 767d68a3
...@@ -41,7 +41,7 @@ def sigmoid_blend_naive_loop(colors, fragments, blend_params): ...@@ -41,7 +41,7 @@ def sigmoid_blend_naive_loop(colors, fragments, blend_params):
pixel_colors[n, h, w, :3] = colors[n, h, w, 0, :] pixel_colors[n, h, w, :3] = colors[n, h, w, 0, :]
pixel_colors[n, h, w, 3] = 1.0 - alpha pixel_colors[n, h, w, 3] = 1.0 - alpha
return torch.flip(pixel_colors, [1]) return pixel_colors
def sigmoid_blend_naive_loop_backward( def sigmoid_blend_naive_loop_backward(
...@@ -54,8 +54,6 @@ def sigmoid_blend_naive_loop_backward( ...@@ -54,8 +54,6 @@ def sigmoid_blend_naive_loop_backward(
N, H, W, K = pix_to_face.shape N, H, W, K = pix_to_face.shape
device = pix_to_face.device device = pix_to_face.device
grad_distances = torch.zeros((N, H, W, K), dtype=dists.dtype, device=device) grad_distances = torch.zeros((N, H, W, K), dtype=dists.dtype, device=device)
images = torch.flip(images, [1])
grad_images = torch.flip(grad_images, [1])
for n in range(N): for n in range(N):
for h in range(H): for h in range(H):
...@@ -130,7 +128,7 @@ def softmax_blend_naive(colors, fragments, blend_params): ...@@ -130,7 +128,7 @@ def softmax_blend_naive(colors, fragments, blend_params):
pixel_colors[n, h, w, :3] += (delta / denom) * bk_color pixel_colors[n, h, w, :3] += (delta / denom) * bk_color
pixel_colors[n, h, w, 3] = 1.0 - alpha pixel_colors[n, h, w, 3] = 1.0 - alpha
return torch.flip(pixel_colors, [1]) return pixel_colors
class TestBlending(unittest.TestCase): class TestBlending(unittest.TestCase):
......
...@@ -173,12 +173,12 @@ class TestCameraHelpers(unittest.TestCase): ...@@ -173,12 +173,12 @@ class TestCameraHelpers(unittest.TestCase):
grad_dist = ( grad_dist = (
torch.cos(elev) * torch.sin(azim) torch.cos(elev) * torch.sin(azim)
+ torch.sin(elev) + torch.sin(elev)
- torch.cos(elev) * torch.cos(azim) + torch.cos(elev) * torch.cos(azim)
) )
grad_elev = ( grad_elev = (
-torch.sin(elev) * torch.sin(azim) -torch.sin(elev) * torch.sin(azim)
+ torch.cos(elev) + torch.cos(elev)
+ torch.sin(elev) * torch.cos(azim) - torch.sin(elev) * torch.cos(azim)
) )
grad_elev = dist * (math.pi / 180.0) * grad_elev grad_elev = dist * (math.pi / 180.0) * grad_elev
self.assertTrue(torch.allclose(elev_grad, grad_elev)) self.assertTrue(torch.allclose(elev_grad, grad_elev))
...@@ -232,12 +232,12 @@ class TestCameraHelpers(unittest.TestCase): ...@@ -232,12 +232,12 @@ class TestCameraHelpers(unittest.TestCase):
grad_dist = ( grad_dist = (
torch.cos(elev) * torch.sin(azim) torch.cos(elev) * torch.sin(azim)
+ torch.sin(elev) + torch.sin(elev)
- torch.cos(elev) * torch.cos(azim) + torch.cos(elev) * torch.cos(azim)
) )
grad_elev = ( grad_elev = (
-torch.sin(elev) * torch.sin(azim) -torch.sin(elev) * torch.sin(azim)
+ torch.cos(elev) + torch.cos(elev)
+ torch.sin(elev) * torch.cos(azim) - torch.sin(elev) * torch.cos(azim)
) )
grad_elev = (dist * (math.pi / 180.0) * grad_elev).sum() grad_elev = (dist * (math.pi / 180.0) * grad_elev).sum()
self.assertTrue(torch.allclose(elev_grad, grad_elev)) self.assertTrue(torch.allclose(elev_grad, grad_elev))
......
This diff is collapsed.
...@@ -72,20 +72,25 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -72,20 +72,25 @@ class TestRenderingMeshes(unittest.TestCase):
# Init rasterizer settings # Init rasterizer settings
if elevated_camera: if elevated_camera:
R, T = look_at_view_transform(2.7, 45.0, 0.0) # Elevated and rotated camera
R, T = look_at_view_transform(dist=2.7, elev=45.0, azim=45.0)
postfix = "_elevated_camera" postfix = "_elevated_camera"
# If y axis is up, the spot of light should
# be on the bottom left of the sphere.
else: else:
# No elevation or azimuth rotation
R, T = look_at_view_transform(2.7, 0.0, 0.0) R, T = look_at_view_transform(2.7, 0.0, 0.0)
postfix = "" postfix = ""
cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T) cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T)
raster_settings = RasterizationSettings(
image_size=512, blur_radius=0.0, faces_per_pixel=1, bin_size=0
)
# Init shader settings # Init shader settings
materials = Materials(device=device) materials = Materials(device=device)
lights = PointLights(device=device) lights = PointLights(device=device)
lights.location = torch.tensor([0.0, 0.0, -2.0], device=device)[None] lights.location = torch.tensor([0.0, 0.0, +2.0], device=device)[None]
raster_settings = RasterizationSettings(
image_size=512, blur_radius=0.0, faces_per_pixel=1, bin_size=0
)
# Init renderer # Init renderer
rasterizer = MeshRasterizer( rasterizer = MeshRasterizer(
...@@ -107,15 +112,16 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -107,15 +112,16 @@ class TestRenderingMeshes(unittest.TestCase):
# Load reference image # Load reference image
image_ref_phong = load_rgb_image( image_ref_phong = load_rgb_image(
"test_simple_sphere_illuminated%s.png" % postfix "test_simple_sphere_light%s.png" % postfix
) )
self.assertTrue(torch.allclose(rgb, image_ref_phong, atol=0.05)) self.assertTrue(torch.allclose(rgb, image_ref_phong, atol=0.05))
################################### ########################################################
# Move the light behind the object # Move the light to the +z axis in world space so it is
################################### # behind the sphere. Note that +Z is in, +Y up,
# Check the image is dark # +X left for both world and camera space.
lights.location[..., 2] = +2.0 ########################################################
lights.location[..., 2] = -2.0
images = renderer(sphere_mesh, lights=lights) images = renderer(sphere_mesh, lights=lights)
rgb = images[0, ..., :3].squeeze().cpu() rgb = images[0, ..., :3].squeeze().cpu()
if DEBUG: if DEBUG:
...@@ -133,7 +139,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -133,7 +139,7 @@ class TestRenderingMeshes(unittest.TestCase):
###################################### ######################################
# Change the shader to a GouraudShader # Change the shader to a GouraudShader
###################################### ######################################
lights.location = torch.tensor([0.0, 0.0, -2.0], device=device)[None] lights.location = torch.tensor([0.0, 0.0, +2.0], device=device)[None]
renderer = MeshRenderer( renderer = MeshRenderer(
rasterizer=rasterizer, rasterizer=rasterizer,
shader=HardGouraudShader( shader=HardGouraudShader(
...@@ -143,7 +149,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -143,7 +149,7 @@ class TestRenderingMeshes(unittest.TestCase):
images = renderer(sphere_mesh) images = renderer(sphere_mesh)
rgb = images[0, ..., :3].squeeze().cpu() rgb = images[0, ..., :3].squeeze().cpu()
if DEBUG: if DEBUG:
filename = "DEBUG_simple_sphere_light_gourad%s.png" % postfix filename = "DEBUG_simple_sphere_light_gouraud%s.png" % postfix
Image.fromarray((rgb.numpy() * 255).astype(np.uint8)).save( Image.fromarray((rgb.numpy() * 255).astype(np.uint8)).save(
DATA_DIR / filename DATA_DIR / filename
) )
...@@ -157,7 +163,6 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -157,7 +163,6 @@ class TestRenderingMeshes(unittest.TestCase):
###################################### ######################################
# Change the shader to a HardFlatShader # Change the shader to a HardFlatShader
###################################### ######################################
lights.location = torch.tensor([0.0, 0.0, -2.0], device=device)[None]
renderer = MeshRenderer( renderer = MeshRenderer(
rasterizer=rasterizer, rasterizer=rasterizer,
shader=HardFlatShader( shader=HardFlatShader(
...@@ -217,7 +222,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -217,7 +222,7 @@ class TestRenderingMeshes(unittest.TestCase):
# Init shader settings # Init shader settings
materials = Materials(device=device) materials = Materials(device=device)
lights = PointLights(device=device) lights = PointLights(device=device)
lights.location = torch.tensor([0.0, 0.0, -2.0], device=device)[None] lights.location = torch.tensor([0.0, 0.0, +2.0], device=device)[None]
# Init renderer # Init renderer
renderer = MeshRenderer( renderer = MeshRenderer(
...@@ -231,7 +236,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -231,7 +236,7 @@ class TestRenderingMeshes(unittest.TestCase):
images = renderer(sphere_meshes) images = renderer(sphere_meshes)
# Load ref image # Load ref image
image_ref = load_rgb_image("test_simple_sphere_illuminated.png") image_ref = load_rgb_image("test_simple_sphere_light.png")
for i in range(batch_size): for i in range(batch_size):
rgb = images[i, ..., :3].squeeze().cpu() rgb = images[i, ..., :3].squeeze().cpu()
...@@ -261,7 +266,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -261,7 +266,7 @@ class TestRenderingMeshes(unittest.TestCase):
) )
# Init rasterizer settings # Init rasterizer settings
R, T = look_at_view_transform(2.7, 10, 20) R, T = look_at_view_transform(2.7, 0, 0)
cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T) cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T)
# Init renderer # Init renderer
...@@ -275,7 +280,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -275,7 +280,7 @@ class TestRenderingMeshes(unittest.TestCase):
alpha = images[0, ..., 3].squeeze().cpu() alpha = images[0, ..., 3].squeeze().cpu()
if DEBUG: if DEBUG:
Image.fromarray((alpha.numpy() * 255).astype(np.uint8)).save( Image.fromarray((alpha.numpy() * 255).astype(np.uint8)).save(
DATA_DIR / "DEBUG_silhouette_grad.png" DATA_DIR / "DEBUG_silhouette.png"
) )
with Image.open(image_ref_filename) as raw_image_ref: with Image.open(image_ref_filename) as raw_image_ref:
...@@ -292,7 +297,8 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -292,7 +297,8 @@ class TestRenderingMeshes(unittest.TestCase):
def test_texture_map(self): def test_texture_map(self):
""" """
Test a mesh with a texture map is loaded and rendered correctly Test a mesh with a texture map is loaded and rendered correctly.
The pupils in the eyes of the cow should always be looking to the left.
""" """
device = torch.device("cuda:0") device = torch.device("cuda:0")
DATA_DIR = ( DATA_DIR = (
...@@ -304,7 +310,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -304,7 +310,7 @@ class TestRenderingMeshes(unittest.TestCase):
mesh = load_objs_as_meshes([obj_filename], device=device) mesh = load_objs_as_meshes([obj_filename], device=device)
# Init rasterizer settings # Init rasterizer settings
R, T = look_at_view_transform(2.7, 10, 20) R, T = look_at_view_transform(2.7, 0, 0)
cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T) cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T)
raster_settings = RasterizationSettings( raster_settings = RasterizationSettings(
image_size=512, blur_radius=0.0, faces_per_pixel=1, bin_size=0 image_size=512, blur_radius=0.0, faces_per_pixel=1, bin_size=0
...@@ -313,7 +319,10 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -313,7 +319,10 @@ class TestRenderingMeshes(unittest.TestCase):
# Init shader settings # Init shader settings
materials = Materials(device=device) materials = Materials(device=device)
lights = PointLights(device=device) lights = PointLights(device=device)
lights.location = torch.tensor([0.0, 0.0, -2.0], device=device)[None]
# Place light behind the cow in world space. The front of
# the cow is facing the -z direction.
lights.location = torch.tensor([0.0, 0.0, 2.0], device=device)[None]
# Init renderer # Init renderer
renderer = MeshRenderer( renderer = MeshRenderer(
...@@ -328,18 +337,13 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -328,18 +337,13 @@ class TestRenderingMeshes(unittest.TestCase):
rgb = images[0, ..., :3].squeeze().cpu() rgb = images[0, ..., :3].squeeze().cpu()
# Load reference image # Load reference image
image_ref = load_rgb_image("test_texture_map.png") image_ref = load_rgb_image("test_texture_map_back.png")
if DEBUG: if DEBUG:
Image.fromarray((rgb.numpy() * 255).astype(np.uint8)).save( Image.fromarray((rgb.numpy() * 255).astype(np.uint8)).save(
DATA_DIR / "DEBUG_texture_map.png" DATA_DIR / "DEBUG_texture_map_back.png"
) )
# There's a calculation instability on the corner of the ear of the cow.
# We ignore that pixel.
image_ref[137, 166] = 0
rgb[137, 166] = 0
self.assertTrue(torch.allclose(rgb, image_ref, atol=0.05)) self.assertTrue(torch.allclose(rgb, image_ref, atol=0.05))
# Check grad exists # Check grad exists
...@@ -352,10 +356,31 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -352,10 +356,31 @@ class TestRenderingMeshes(unittest.TestCase):
images[0, ...].sum().backward() images[0, ...].sum().backward()
self.assertIsNotNone(verts.grad) self.assertIsNotNone(verts.grad)
##########################################
# Check rendering of the front of the cow
##########################################
R, T = look_at_view_transform(2.7, 0, 180)
cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T)
# Move light to the front of the cow in world space
lights.location = torch.tensor([0.0, 0.0, -2.0], device=device)[None]
images = renderer(mesh, cameras=cameras, lights=lights)
rgb = images[0, ..., :3].squeeze().cpu()
# Load reference image
image_ref = load_rgb_image("test_texture_map_front.png")
if DEBUG:
Image.fromarray((rgb.numpy() * 255).astype(np.uint8)).save(
DATA_DIR / "DEBUG_texture_map_front.png"
)
################################# #################################
# Add blurring to rasterization # Add blurring to rasterization
################################# #################################
R, T = look_at_view_transform(2.7, 0, 180)
cameras = OpenGLPerspectiveCameras(device=device, R=R, T=T)
blend_params = BlendParams(sigma=5e-4, gamma=1e-4) blend_params = BlendParams(sigma=5e-4, gamma=1e-4)
raster_settings = RasterizationSettings( raster_settings = RasterizationSettings(
image_size=512, image_size=512,
...@@ -366,6 +391,7 @@ class TestRenderingMeshes(unittest.TestCase): ...@@ -366,6 +391,7 @@ class TestRenderingMeshes(unittest.TestCase):
images = renderer( images = renderer(
mesh.clone(), mesh.clone(),
cameras=cameras,
raster_settings=raster_settings, raster_settings=raster_settings,
blend_params=blend_params, blend_params=blend_params,
) )
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment