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

reset_camera does not correctly compute the distance #1079

Open
sergei9838 opened this issue Mar 23, 2024 · 1 comment
Open

reset_camera does not correctly compute the distance #1079

sergei9838 opened this issue Mar 23, 2024 · 1 comment

Comments

@sergei9838
Copy link

sergei9838 commented Mar 23, 2024

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

@sergei9838 sergei9838 changed the title reset_camera does not correctly compute the size reset_camera does not correctly compute the distance Mar 23, 2024
marcomusy added a commit that referenced this issue Mar 25, 2024
@marcomusy
Copy link
Owner

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants