marcomusy / vedo

A python module for scientific analysis of 3D data based on VTK and Numpy
https://vedo.embl.es
MIT License
2.03k stars 265 forks source link

reset_camera does not correctly compute the distance #1079

Closed sergei9838 closed 3 months ago

sergei9838 commented 6 months ago

Hi, Marco, I was puzzled why plotting an image (Image) produces a visible padding space around even at zoom="tightest" which is supposed to be giving a negligible 0.0001 size margin. Even if Plotter is instantiated with the exact image size. Take an example:

plt = Plotter()
im = Image(dataurl + 'dog.jpg')
print(f'Image dimensions: {im.dimensions()}')
# Image dimensions: [581 723]
plt.add(im)

plt.show(zoom="tightest")

cam = plt.camera
print(f'pos: {cam.GetPosition()}, dist: {cam.GetDistance()}')
print(f'angle: {cam.GetViewAngle()}, view-up: {cam.GetViewUp()}')
# pos: (289.71, 360.639, 1393.5414364778803), dist: 1393.5414364778803
# angle: 30.0, view-up: (0.0, 1.0, 0.0)
Screenshot 2024-03-23 at 22 12 58

There is a visible padding even on the top and the bottom of the image and (289.71, 360.639) is not the centre of the 581 by 723 image because of the padding.

Since plotting with zoom="tightest" calls reset_camera method from plotter.py, let's go along its lines:

x0, x1, y0, y1, z0, z1 = self.renderer.ComputeVisiblePropBounds()
print(f'{x0=}, {x1=}, {y0=}, {y1=}')
# x0=0.0, x1=580.0, y0=0.0, y1=722.0

Thus x1 and y1 are the last pixel coordinates, but the image size is not x1-x0 by y1-y0: it should be x1-x0+1 by y1-y0+1 below:

dx, dy = (x1 - x0) * 0.999, (y1 - y0) * 0.999
plt.renderer.ComputeAspect()
aspect = plt.renderer.GetAspect()
angle = np.pi * cam.GetViewAngle() / 180.0

I do not know the rational behind 0.999 though. In the next line:

dist = max(dx / aspect[0], dy) / np.sin(angle / 2) / 2

np.sin should definitely be np.tan. The difference is not so small: 1393.4 vs. 1345.9

There are also the following problems.

Plot a 2D-image:


plt = Plotter(size=(581, 723))
im2 = Image(dataurl + 'dog.jpg').clone2d()
plt.add(im2)

plt.show(zoom=0.)

cam = plt.camera
print(f'pos: {cam.GetPosition()}, dist: {cam.GetDistance()}')
print(f'angle: {cam.GetViewAngle()}, view-up: {cam.GetViewUp()}')
# pos: (0.0, 0.0, 1.0), dist: 1.0
# angle: 30.0, view-up: (0.0, 1.0, 0.0)

plt.renderer.ComputeAspect()
aspect = plt.renderer.GetAspect()
angle = np.pi * cam.GetViewAngle() / 180.0
x0, x1, y0, y1, z0, z1 = plt.renderer.ComputeVisiblePropBounds()
print(x0, x1, y0, y1, z0, z1)
# 1.0 -1.0 1.0 -1.0 1.0 -1.0

so the bounds are in reversed order, making dx, dy = x1 - x0, y1 - y0 and distance negative:

dist = (max(dx / aspect[0], dy)) / np.tan(angle / 2) / 2
print(f'{dist=}')
# dist=-3.7320508075688776
cam.SetViewUp(0, 1, 0)
cam.SetPosition(x0 + dx / 2, y0 + dy / 2, dist)
cam.SetFocalPoint(x0 + dx / 2, y0 + dy / 2, 0)
print(f'pos: {cam.GetPosition()}, dist: {cam.GetDistance()}')
# pos: (0.0, 0.0, -3.7320508075688776), dist: 3.7320508075688776

Thus the camera after reset has jumped to the opposite side of the XY-plane.

The following is a suggested fix:

def reset_camera(self, tight=None) -> "Plotter":
    """
    Reset the camera position and zooming.
    If tight (float) is specified the zooming reserves a padding space
    in the xy-plane expressed in percent of the average size.
    """
    if tight is None:
        self.renderer.ResetCamera()
    else:
        x0, x1, y0, y1, z0, z1 = self.renderer.ComputeVisiblePropBounds()

        cam = self.renderer.GetActiveCamera()

        self.renderer.ComputeAspect()
        aspect = self.renderer.GetAspect()
        angle = np.pi * cam.GetViewAngle() / 180.0
        dx, dy = np.abs(x1 - x0) + 1., np.abs(y1 - y0) + 1.
        dist = max(dx / aspect[0], dy) / np.tan(angle / 2) / 2

        cam.SetViewUp(0, 1, 0)
        cam.SetPosition(x0 + dx / 2, y0 + dy / 2, dist * (1 + tight))
        cam.SetFocalPoint(x0 + dx / 2, y0 + dy / 2, 0)
        if cam.GetParallelProjection():
            ps = max(dx / aspect[0], dy) / 2
            cam.SetParallelScale(ps * (1 + tight))
        self.renderer.ResetCameraClippingRange(x0, x1, y0, y1, z0, z1)
    return self

After this, no padding produced around the image

plt = Plotter(size=(581, 723))
im = Image(dataurl + 'dog.jpg')
plt.add(im)
plt.show(zoom="tightest")
cam = plt.camera
print(f'pos: {cam.GetPosition()}, dist: {cam.GetDistance()}')
# (290.5, 361.5, 1349.271280572843), dist: 1349.271280572843
x0, x1, y0, y1, z0, z1 = plt.renderer.ComputeVisiblePropBounds()
plt.renderer.ComputeAspect()
aspect = plt.renderer.GetAspect()
angle = np.pi * cam.GetViewAngle() / 180.0
dx, dy = np.abs(x1 - x0) + 1., np.abs(y1 - y0) + 1.
dist = (max(dx / aspect[0], dy)) / np.tan(angle / 2) / 2 
print(f'{dist=}')
# dist=1349.1363669361492

the focal point is in the centre of the image: (290.5, 361.5) and the distance is (almost) as expected!

A general comment: calling argument zoom in Plotter.show is misleading: when you zoom a camera lens you do not change the position, but the view angle. This is also what Zoom function in vtk does as I can see. But zoom when passed to reset_camera above moves the camera position instead, leaving the angle the same. I would suggest using padding parameter or just the very tight parameter of reset_camera in show which could also accept a float "expressed in percent of the average size". Parameter zoom should change the view angle and padding could then preserve the angle and change the distance.

Thank you, Marco, for your great work! Sergei + Eric

marcomusy commented 6 months ago

Thanks both!! This is absolutely fantastic... I could not debug myself that one :) I think though the +1 should not be there (rather you should subtract 1 from the show(), otherwise it would not work with scaling objects). Just pushed to dev09:

from vedo import *
# settings.use_parallel_projection = True
img = Image(dataurl + 'dog.jpg')#.scale(0.01)
print(f'Image dimensions: {img.dimensions()}')
show(img, zoom="tightest", size=img.dimensions()-1)

Screenshot from 2024-03-25 22-59-09

A general comment: calling argument zoom in Plotter.show is misleading: when you zoom a camera lens you do not change the position, but the view angle. This is also what Zoom function in vtk does as I can see.

thanks for this too, I will definitely look at what can be done to address that.