|
|
|
|
|
|
|
|
|
|
|
|
|
import unittest |
|
|
|
import torch |
|
from pytorch3d.renderer import ( |
|
MeshRasterizer, |
|
NDCMultinomialRaysampler, |
|
PerspectiveCameras, |
|
PointsRasterizationSettings, |
|
PointsRasterizer, |
|
PulsarPointsRenderer, |
|
RasterizationSettings, |
|
) |
|
from pytorch3d.structures import Meshes, Pointclouds |
|
|
|
from .common_testing import TestCaseMixin |
|
|
|
|
|
""" |
|
PyTorch3D renderers operate in an align_corners=False manner. |
|
This file demonstrates the pixel-perfect calculation by very simple |
|
examples. |
|
""" |
|
|
|
|
|
class _CommonData: |
|
""" |
|
Contains data for all these tests. |
|
|
|
- Firstly, a non-square at the origin specified in ndc space and |
|
screen space. Principal point is in the center of the image. |
|
Focal length is 1.0 in world space. |
|
This camera has the identity as its world to view transformation, so |
|
it is facing down the positive z axis with y being up and x being left. |
|
A point on the z=1.0 focal plane has its x,y world coordinate equal to |
|
its NDC. |
|
|
|
- Secondly, batched together with that, is a camera with the same |
|
focal length facing in the same direction but located so that it faces |
|
the corner of the corner pixel of the first image, with its principal |
|
point located at its corner, so that it maps the z=1 plane to NDC just |
|
like the first. |
|
|
|
- a single point self.point in world space which is located on a plane 1.0 |
|
in front from the camera which is located exactly in the center |
|
of a known pixel (self.x, self.y), specifically with negative x and slightly |
|
positive y, so it is in the top right quadrant of the image. |
|
|
|
- A second batch of cameras defined in screen space which exactly match the |
|
first ones. |
|
|
|
So that this data can be copied for making demos, it is easiest to leave |
|
it as a freestanding class. |
|
""" |
|
|
|
def __init__(self): |
|
self.H, self.W = 249, 125 |
|
self.image_size = (self.H, self.W) |
|
self.camera_ndc = PerspectiveCameras( |
|
focal_length=1.0, |
|
image_size=(self.image_size,), |
|
in_ndc=True, |
|
T=torch.tensor([[0.0, 0.0, 0.0], [-1.0, self.H / self.W, 0.0]]), |
|
principal_point=((-0.0, -0.0), (1.0, -self.H / self.W)), |
|
) |
|
|
|
self.camera_screen = PerspectiveCameras( |
|
focal_length=self.W / 2.0, |
|
principal_point=((self.W / 2.0, self.H / 2.0), (0.0, self.H)), |
|
image_size=(self.image_size,), |
|
T=torch.tensor([[0.0, 0.0, 0.0], [-1.0, self.H / self.W, 0.0]]), |
|
in_ndc=False, |
|
) |
|
|
|
|
|
self.x, self.y = 81, 113 |
|
self.point = [-0.304, 0.176, 1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class TestPixels(TestCaseMixin, unittest.TestCase): |
|
def test_mesh(self): |
|
data = _CommonData() |
|
|
|
|
|
verts = torch.tensor( |
|
[[-0.288, 0.192, 1], [-0.32, 0.192, 1], [-0.304, 0.144, 1]] |
|
) |
|
self.assertClose(verts.mean(0), torch.tensor(data.point)) |
|
faces = torch.LongTensor([[0, 1, 2]]) |
|
|
|
|
|
meshes = Meshes(verts=[verts], faces=[faces]).extend(2) |
|
faces_per_pixel = 2 |
|
for camera in (data.camera_ndc, data.camera_screen): |
|
rasterizer = MeshRasterizer( |
|
cameras=camera, |
|
raster_settings=RasterizationSettings( |
|
image_size=data.image_size, faces_per_pixel=faces_per_pixel |
|
), |
|
) |
|
barycentric_coords_found = rasterizer(meshes).bary_coords |
|
self.assertTupleEqual( |
|
barycentric_coords_found.shape, |
|
(2,) + data.image_size + (faces_per_pixel, 3), |
|
) |
|
|
|
|
|
|
|
self.assertClose( |
|
barycentric_coords_found[:, data.y, data.x, 0], |
|
torch.full((2, 3), 1 / 3.0), |
|
atol=1e-5, |
|
) |
|
|
|
def test_pointcloud(self): |
|
data = _CommonData() |
|
clouds = Pointclouds(points=torch.tensor([[data.point]])).extend(2) |
|
colorful_cloud = Pointclouds( |
|
points=torch.tensor([[data.point]]), features=torch.ones(1, 1, 3) |
|
).extend(2) |
|
points_per_pixel = 2 |
|
|
|
for camera in (data.camera_ndc, data.camera_screen): |
|
rasterizer = PointsRasterizer( |
|
cameras=camera, |
|
raster_settings=PointsRasterizationSettings( |
|
image_size=data.image_size, |
|
radius=0.0001, |
|
points_per_pixel=points_per_pixel, |
|
), |
|
) |
|
|
|
rasterizer_output = rasterizer(clouds).idx |
|
self.assertTupleEqual( |
|
rasterizer_output.shape, (2,) + data.image_size + (points_per_pixel,) |
|
) |
|
found = torch.nonzero(rasterizer_output != -1) |
|
self.assertTupleEqual(found.shape, (2, 4)) |
|
self.assertListEqual(found[0].tolist(), [0, data.y, data.x, 0]) |
|
self.assertListEqual(found[1].tolist(), [1, data.y, data.x, 0]) |
|
|
|
if camera.in_ndc(): |
|
|
|
pulsar_renderer = PulsarPointsRenderer(rasterizer=rasterizer) |
|
pulsar_output = pulsar_renderer( |
|
colorful_cloud, gamma=(0.1, 0.1), znear=(0.1, 0.1), zfar=(70, 70) |
|
) |
|
self.assertTupleEqual( |
|
pulsar_output.shape, (2,) + data.image_size + (3,) |
|
) |
|
|
|
|
|
|
|
found = torch.nonzero(pulsar_output[0, :, :, 0]) |
|
self.assertTupleEqual(found.shape, (1, 2)) |
|
self.assertListEqual(found[0].tolist(), [data.y, data.x]) |
|
|
|
|
|
|
|
|
|
|
|
|
|
def test_raysampler(self): |
|
data = _CommonData() |
|
gridsampler = NDCMultinomialRaysampler( |
|
image_width=data.W, |
|
image_height=data.H, |
|
n_pts_per_ray=2, |
|
min_depth=1.0, |
|
max_depth=2.0, |
|
) |
|
for camera in (data.camera_ndc, data.camera_screen): |
|
bundle = gridsampler(camera) |
|
self.assertTupleEqual(bundle.xys.shape, (2,) + data.image_size + (2,)) |
|
self.assertTupleEqual( |
|
bundle.directions.shape, (2,) + data.image_size + (3,) |
|
) |
|
self.assertClose( |
|
bundle.xys[:, data.y, data.x], |
|
torch.tensor(data.point[:2]).expand(2, -1), |
|
) |
|
|
|
|
|
self.assertClose( |
|
bundle.directions[0, data.y, data.x], |
|
torch.tensor(data.point), |
|
) |
|
|
|
def test_camera(self): |
|
data = _CommonData() |
|
|
|
|
|
points = torch.tensor([data.point, [0, 0, 1], [1, data.H / data.W, 1]]) |
|
for cameras in (data.camera_ndc, data.camera_screen): |
|
ndc_points = cameras.transform_points_ndc(points) |
|
screen_points = cameras.transform_points_screen(points) |
|
screen_points_without_xyflip = cameras.transform_points_screen( |
|
points, with_xyflip=False |
|
) |
|
camera_points = cameras.transform_points(points) |
|
for batch_idx in range(2): |
|
|
|
self.assertClose(ndc_points[batch_idx], points, atol=1e-5) |
|
|
|
self.assertClose( |
|
screen_points[batch_idx][0], |
|
torch.tensor([data.x + 0.5, data.y + 0.5, 1.0]), |
|
atol=1e-5, |
|
) |
|
|
|
|
|
self.assertClose( |
|
screen_points_without_xyflip[batch_idx][0], |
|
torch.tensor([-(data.x + 0.5), -(data.y + 0.5), 1.0]), |
|
atol=1e-5, |
|
) |
|
|
|
self.assertClose( |
|
screen_points[batch_idx][1], |
|
torch.tensor([data.W / 2.0, data.H / 2.0, 1.0]), |
|
atol=1e-5, |
|
) |
|
|
|
|
|
self.assertClose( |
|
screen_points[batch_idx][2], |
|
torch.tensor([0.0, 0.0, 1.0]), |
|
atol=1e-5, |
|
) |
|
|
|
if cameras.in_ndc(): |
|
self.assertClose(camera_points[batch_idx], ndc_points[batch_idx]) |
|
else: |
|
|
|
if batch_idx == 0: |
|
wanted = torch.stack( |
|
[ |
|
data.W - screen_points[batch_idx, :, 0], |
|
data.H - screen_points[batch_idx, :, 1], |
|
torch.ones(3), |
|
], |
|
dim=1, |
|
) |
|
else: |
|
wanted = torch.stack( |
|
[ |
|
-screen_points[batch_idx, :, 0], |
|
2 * data.H - screen_points[batch_idx, :, 1], |
|
torch.ones(3), |
|
], |
|
dim=1, |
|
) |
|
self.assertClose(camera_points[batch_idx], wanted) |
|
|