Modifying a filter's output array?

Hi. I like the scaling behavior I get when using the vtk.vtkDistanceToCamera() filter on my point cloud that’s zoomed out. It keeps my points visible. But I’d like it to quit this scaling when I zoom in. Basically I think I want the array values produced by this filter to not drop below 1.

Is there an existing filter I can use to do this? If not, I think I need to manually modify the array. But how do I access this array?

Thanks, and here is some demo python code where I’m using vtkDistanceToCamera.

import vtk

renderer = vtk.vtkRenderer()

points_b = vtk.vtkPoints()

for x in range(0, 3):
    for y in range(0, 4):
        for z in range(0, 3):
            points_b.InsertNextPoint(x*40 + 6, y*40, z*40)

poly_data_b = vtk.vtkPolyData()
poly_data_b.SetPoints(points_b)

sphere = vtk.vtkSphereSource()
sphere.SetPhiResolution(24)
sphere.SetThetaResolution(24)
sphere.SetRadius(3)

camera_dist_b = vtk.vtkDistanceToCamera()
camera_dist_b.SetInputData(poly_data_b)
camera_dist_b.SetRenderer(renderer)
camera_dist_b.SetScreenSize(3)

#a filter here to cap camera_dist_b's output to greater than one?

mapper_b = vtk.vtkGlyph3DMapper()
mapper_b.SetInputData(poly_data_b)
mapper_b.SetSourceConnection(sphere.GetOutputPort())
mapper_b.SetInputConnection(camera_dist_b.GetOutputPort())
mapper_b.SetInputArrayToProcess(0, 0, 0, vtk.vtkDataObject.FIELD_ASSOCIATION_POINTS, 'DistanceToCamera')

actor_b = vtk.vtkActor()
actor_b.SetMapper(mapper_b)
actor_b.GetProperty().SetColor(0, 1, 0)
renderer.AddActor(actor_b)

render_window = vtk.vtkRenderWindow()
render_window.AddRenderer(renderer)
interactor = vtk.vtkRenderWindowInteractor()
style = vtk.vtkInteractorStyleTrackballCamera()
interactor.SetInteractorStyle(style)
interactor.SetRenderWindow(render_window)
renderer.SetBackground(1, 1, 1)
renderer.ResetCamera()
renderer.GetActiveCamera().Zoom(8)
render_window.Render()
interactor.Start()

You could first try thresholding the scaling factors manually using numpy. If you get the desired effect, then you may consider adding this feature to vtkDistanceToCamera class (probably it can be implemented in 10-20 lines) and submit a merge request to the official VTK repository.

In 3D Slicer markups we use a slightly different approach: We don’t use distance filter, this way points that are closer to the camera always appear somewhat larger (this is an important 3D hint). To prevent markups to become invisible or become so large that they occlude everything else, we set constant scaling based on scaling factor at the camera focal point. We also give users the option to switch between true physical size and this normalized scale. See implementation here: https://github.com/Slicer/Slicer/blob/master/Modules/Loadable/Markups/VTKWidgets/vtkSlicerMarkupsWidgetRepresentation3D.cxx

That’s an interesting approach with 3D Slicer.

You could first try thresholding the scaling factors manually using numpy.

I would like to try this, but I don’t know how to get access to this array in order to manipulate it. If I understand, this manipulation would need to occur in a custom filter that sits between vtkDistanceToCamera and vtkGlyph3DMapper, since the output of d2c changes when the camera moves.

No need to create a custom filter. You can just observe camera changes, perform the necessary updates, and request a render (this is how it works in Slicer, too).

You can get read/write access to VTK arrays as numpy arrays using vtk.util.numpy_support. See example usage here.

I’ve made some progress, thanks.
I can now access and modify the DistanceToCamera array when the interactor is modified.
However, it has no effect on the rendering. And if I read out the array after the renderer is closed, I see it contains unclipped values.

I suspect that this array is getting overwritten with its normal values AFTER I modify it and then the render is using it. Any ideas how I can change this order of operations?

Here is the code so far with the camera callback up top.

import vtk
import vtk.util.numpy_support
import numpy


def clip_distance_array(self, event):
    print(event)

    vtk_array = camera_dist_b.GetOutputDataObject(0).GetPointData().GetArray('DistanceToCamera')
    numpy_array = vtk.util.numpy_support.vtk_to_numpy(vtk_array)

    numpy.clip(numpy_array, 1, 100, out=numpy_array)

    vtk_array.DataChanged()
    vtk_array.Modified()
    
    camera_dist_b.Modified()
    mapper_b.Modified()
    
    camera_dist_b.Update()
    mapper_b.Update()

    render_window.Render()

    print(numpy_array)
    print(vtk_array.GetValueRange())



renderer = vtk.vtkRenderer()

points_b = vtk.vtkPoints()

for x in range(0, 3):
    for y in range(0, 4):
        for z in range(0, 3):
            points_b.InsertNextPoint(x*40 + 6, y*40, z*40)

poly_data_b = vtk.vtkPolyData()
poly_data_b.SetPoints(points_b)

sphere = vtk.vtkSphereSource()
sphere.SetPhiResolution(24)
sphere.SetThetaResolution(24)
sphere.SetRadius(3)

camera_dist_b = vtk.vtkDistanceToCamera()
camera_dist_b.SetInputData(poly_data_b)
camera_dist_b.SetRenderer(renderer)
camera_dist_b.SetScreenSize(3)

mapper_b = vtk.vtkGlyph3DMapper()
mapper_b.SetInputData(poly_data_b)
mapper_b.SetSourceConnection(sphere.GetOutputPort())
mapper_b.SetInputConnection(camera_dist_b.GetOutputPort())
mapper_b.SetInputArrayToProcess(0, 0, 0, vtk.vtkDataObject.FIELD_ASSOCIATION_POINTS, 'DistanceToCamera')

actor_b = vtk.vtkActor()
actor_b.SetMapper(mapper_b)
actor_b.GetProperty().SetColor(0, 1, 0)
renderer.AddActor(actor_b)

render_window = vtk.vtkRenderWindow()
render_window.AddRenderer(renderer)
interactor = vtk.vtkRenderWindowInteractor()
style = vtk.vtkInteractorStyleTrackballCamera()
interactor.SetInteractorStyle(style)
interactor.SetRenderWindow(render_window)
renderer.SetBackground(1, 1, 1)
renderer.ResetCamera()
renderer.GetActiveCamera().Zoom(8)

interactor.AddObserver("ModifiedEvent", clip_distance_array)

render_window.Render()
interactor.Start()


print('________________________________')
print('Final look at the array:')

vtk_array = camera_dist_b.GetOutputDataObject(0).GetPointData().GetArray('DistanceToCamera')

print(vtk_array.GetValueRange())
for index in range(vtk_array.GetNumberOfValues()):
    print(vtk_array.GetValue(index))

I’m very close to achieving my goal. Only now my rendering is flashing between my desired clipped scaling and the normal scaling that vtkDistanceToCamera provides while I interact with the vtkRenderWindowInteractor. When I let go of the mouse, the desired clipped scaling is shown.

I made this progress by changing the clipping code to be tied to the RenderEvent instead of the ModifiedEvent and removing the Update/Modified calls on the vtkDistanceToCamera and vtkGlyph3DMapper objects in my clipping code.

I think to finish this off I just need to ensure my clipping code gets executed every time before a rendering happens… but how if RenderEvent doesn’t cut it? Tieing it to InteractionEvent doesn’t help either.

Here’s the current status code:

import vtk
import vtk.util.numpy_support
import numpy


def clip_distance_array(self, event):
    print(event)

    vtk_array = camera_dist_b.GetOutputDataObject(0).GetPointData().GetArray('DistanceToCamera')
    numpy_array = vtk.util.numpy_support.vtk_to_numpy(vtk_array)

    numpy.clip(numpy_array, 1, 100, out=numpy_array)

    vtk_array.DataChanged()
    vtk_array.Modified()
    
    print(numpy_array)
    print(vtk_array.GetValueRange())


renderer = vtk.vtkRenderer()

points_b = vtk.vtkPoints()

for x in range(0, 3):
    for y in range(0, 4):
        for z in range(0, 3):
            points_b.InsertNextPoint(x*40 + 6, y*40, z*40)

poly_data_b = vtk.vtkPolyData()
poly_data_b.SetPoints(points_b)

sphere = vtk.vtkSphereSource()
sphere.SetPhiResolution(24)
sphere.SetThetaResolution(24)
sphere.SetRadius(3)

camera_dist_b = vtk.vtkDistanceToCamera()
camera_dist_b.SetInputData(poly_data_b)
camera_dist_b.SetRenderer(renderer)
camera_dist_b.SetScreenSize(3)

mapper_b = vtk.vtkGlyph3DMapper()
mapper_b.SetInputData(poly_data_b)
mapper_b.SetSourceConnection(sphere.GetOutputPort())
mapper_b.SetInputConnection(camera_dist_b.GetOutputPort())
mapper_b.SetInputArrayToProcess(0, 0, 0, vtk.vtkDataObject.FIELD_ASSOCIATION_POINTS, 'DistanceToCamera')

actor_b = vtk.vtkActor()
actor_b.SetMapper(mapper_b)
actor_b.GetProperty().SetColor(0, 1, 0)
renderer.AddActor(actor_b)

render_window = vtk.vtkRenderWindow()
render_window.AddRenderer(renderer)
interactor = vtk.vtkRenderWindowInteractor()
style = vtk.vtkInteractorStyleTrackballCamera()
interactor.SetInteractorStyle(style)
interactor.SetRenderWindow(render_window)
renderer.SetBackground(1, 1, 1)
renderer.ResetCamera()
renderer.GetActiveCamera().Zoom(8)

#interactor.AddObserver("ModifiedEvent", clip_distance_array)
interactor.AddObserver("RenderEvent", clip_distance_array)
#interactor.AddObserver("InteractionEvent", clip_distance_array)


render_window.Render()
interactor.Start()


print('________________________________')
print('Final look at the array:')

vtk_array = camera_dist_b.GetOutputDataObject(0).GetPointData().GetArray('DistanceToCamera')

print(vtk_array.GetValueRange())
for index in range(vtk_array.GetNumberOfValues()):
    print(vtk_array.GetValue(index))