How to send python backend actor object data to vtk.js in HTML for rendering in web-browser?

Hello everyone,

I was trying to create a 3D web-viewer with the help of the Django framework.
In the backend, I am using python ITK and VTK packages to read a series of DICOM images. And then I am passing the generated actor to the vtk.js on the HTML side for rendering. But I am getting errors related to ‘vtkRenderingOpenGL2Python.vtkOpenGLActor’ and I tried to search in google to find the solution but I couldn’t find any previous document or help related to this topic.

code: Backend side

import os
import vtk
import SimpleITK as sitk
from vtk.util import numpy_support

colors = vtk.vtkNamedColors()
colors.SetColor('SkinColor', [240, 184, 160, 255])
colors.SetColor('BackfaceColor', [255, 229, 200, 255])
colors.SetColor('BkgColor', [51, 77, 102, 255])


# Conversion ITK to VTK
def sitk2vtk(img, debugOn=False):
    """Convert a SimpleITK image to a VTK image, via numpy."""

    size = list(img.GetSize())
    origin = list(img.GetOrigin())
    spacing = list(img.GetSpacing())
    ncomp = img.GetNumberOfComponentsPerPixel()
    direction = img.GetDirection()

    # there doesn't seem to be a way to specify the image orientation in VTK

    # convert the SimpleITK image to a numpy array
    i2 = sitk.GetArrayFromImage(img)
    if debugOn:
        i2_string = i2.tostring()
        print("data string address inside sitk2vtk", hex(id(i2_string)))

    vtk_image = vtk.vtkImageData()

    # VTK expects 3-dimensional parameters
    if len(size) == 2:
        size.append(1)

    if len(origin) == 2:
        origin.append(0.0)

    if len(spacing) == 2:
        spacing.append(spacing[0])

    if len(direction) == 4:
        direction = [direction[0], direction[1], 0.0,
                     direction[2], direction[3], 0.0,
                     0.0, 0.0, 1.0]

    vtk_image.SetDimensions(size)
    vtk_image.SetSpacing(spacing)
    vtk_image.SetOrigin(origin)
    vtk_image.SetExtent(0, size[0] - 1, 0, size[1] - 1, 0, size[2] - 1)

    if vtk.vtkVersion.GetVTKMajorVersion() < 9:
        print("Warning: VTK version <9.  No direction matrix.")
    else:
        vtk_image.SetDirectionMatrix(direction)

    # depth_array = numpy_support.numpy_to_vtk(i2.ravel(), deep=True,
    #                                          array_type = vtktype)
    depth_array = numpy_support.numpy_to_vtk(i2.ravel())
    depth_array.SetNumberOfComponents(ncomp)
    vtk_image.GetPointData().SetScalars(depth_array)

    vtk_image.Modified()
    #
    if debugOn:
        print("Volume object inside sitk2vtk")
        print(vtk_image)
        #        print("type = ", vtktype)
        print("num components = ", ncomp)
        print(size)
        print(origin)
        print(spacing)
        print(vtk_image.GetScalarComponentAsFloat(0, 0, 0, 0))
    return vtk_image


# File reader using ITK
# converter image file from ITK to VTK object
def file_reader(directory):
    reader = sitk.ImageSeriesReader()
    dicom_names = reader.GetGDCMSeriesFileNames(directory)

    l = []
    for file in os.listdir(directory):
        img = directory + file
        l.append(img)

    filename_sorted = sorted(dicom_names, key=lambda i: int(os.path.splitext(os.path.basename(i))[0]))

    reader.SetFileNames(filename_sorted)
    image = reader.Execute()
    ITK_TO_VTK = sitk2vtk(image)
    return ITK_TO_VTK


# Intensity range finder and isovalue creator
def data_range(ITKTOVTK):
    # ITKTOVTK = sitk2vtk(image)
    intensity_range = ITKTOVTK.GetPointData().GetScalars().GetRange()
    isoValue = int((intensity_range[0] + intensity_range[1]) / 3)
    return isoValue


# Surface volume rendering algorithm selector
def contour(itk_vtk_obj, isoVal):
    contour_algo = vtk.vtkMarchingCubes()
    contour_algo.SetInputData(itk_vtk_obj)
    contour_algo.SetValue(0, isoVal)
    contour_algo.ComputeNormalsOn()

    mapper = vtk.vtkPolyDataMapper()
    mapper.ScalarVisibilityOff()
    mapper.SetInputConnection(contour_algo.GetOutputPort())


    actor = vtk.vtkActor()
    actor.SetMapper(mapper)
    # actor.GetProperty().SetColor(222/256., 184/256., 135/256.)
    actor.GetProperty().SetDiffuseColor(colors.GetColor3d('Red'))  # red
    actor.GetProperty().SetSpecular(0.3)
    actor.GetProperty().SetSpecularPower(1)
    actor.GetProperty().SetOpacity(0.9)
    return actor

And in Django app view.py, I passed the bellow lines of code to render

# Image directory: contains series of dicom files
directory = "/Micro_temp_1/visapp/media/1/Channel_-01-0/"

# Create your views here.
def IndexView(request):
    i = file_reader(directory)
    iso_Val = data_range(i)
    iii = contour(i, iso_Val)
    # convert into JSON
    iiii = iii.tolist()
    reader = json.dumps(iiii)
    print(type(iii))
    # iiii = volume_render(iii)

    context = {"r": reader, "text": "h", "array": [4, 5]}
    return render(request, 'visapp/index.html', context)

Inside the Index.HTML page

<script>

    const fullScreenRenderer = vtk.Rendering.Misc.vtkFullScreenRenderWindow.newInstance({background: [0, 0, 0],});
    const renderer = fullScreenRenderer.getRenderer();
    const renderWindow = fullScreenRenderer.getRenderWindow();

    actor = {{r}}
    // console.log(actor)
    renderer.addActor(actor);
    renderer.getActiveCamera().set({ position: [1, 1, 0], viewUp: [0, 0, -1] });
    renderer.resetCamera();

</script>

I am getting a bellow error:

Environment:
Request Method: GET
Request URL: http://127.0.0.1:8000/
Django Version: 3.2.8
Python Version: 3.7.10
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'visapp.apps.VisappConfig']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']

Traceback (most recent call last):
  File "/home/anaconda3/envs/Micro_temp_1/lib/python3.7/site-packages/django/core/handlers/exception.py", line 47, in inner
    response = get_response(request)
  File "/home/anaconda3/envs/Micro_temp_1/lib/python3.7/site-packages/django/core/handlers/base.py", line 181, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/home/PycharmProjects/Micro_temp_1/Micro_temp_1/visapp/views.py", line 14, in IndexView
    iiii = iii.tolist()

Exception Type: AttributeError at /
Exception Value: 'vtkRenderingOpenGL2Python.vtkOpenGLActor' object has no attribute 'tolist'

Is there any way to send the VTK actor object to the vtk.js side for rendering?

You should send the dataset rather than the actor. The actor is just some rendering properties…
Also JSON is not the best format for sending binary content. I’m guessing your contour filter generate a vtkPolyData and now it is up to you to send it over. If you want to do it the naive way, you can use that way to serialize and load it on the js side.

Thank you for the quick reply!

  1. You should send the dataset rather than the actor: I think you mean to say read Dicom files using itk.js and then convert them to vtk object and then use vtk.js. If that is what you are suggesting then I have tried that before and it worked pretty well but I guess there are some functional limitations in VTK.JS like I couldn’t find how can I apply VTK python other isosurface rendering algorithms like contour filer, Flying3D edge beside only marching cube algorithm and same goes for volume rendering, I could not find an example of how can I apply vtkOpenGLGPUVolumeRayCastMapper function in vtk.js and also select it’s blending mode like, Maximum, minimum, etc.

  2. Also JSON is not the best format for sending binary content: As I am pretty new to this topic so due you have any other function in mind besides JSON which I can use to send Vtk objects.

  3. I’m guessing your contour filter generate a vtkPolyData and now it is up to you to send it over: Suggest me something which I can try or suggest to me what would be the right way to fulfil my requirements.

  4. If you want to do it the naive way, you can use that way 1 to serialize and load it on the js side: I saw the actor.json file. Do I need to create it manually or do I need to pass the python vtk actor object through JSON.parse(actor)?

I am working on this for a couple of weeks and still, I didn’t figure out the right direction which I should follow to implement 3D visualization for DICOM images.
Any better suggestions? which I can follow without limitation.
Thank you.

I was saying that you should do the contour on the Python side and send the output of that filter to the client not the DICOM.

I guess since you are not well versed with web tech, it might be easier to use the vtkXMLPolyDataWriter to send the file over the network and use the vtk.js corresponding reader to reload the polydata on the JS side.

Does that help?

Thank you for the reply.

Today, I went through some tutorials of VTK.js and I found I can also use vtkXMLPolyDataWriter to write and save images in .vtp if I want isosurface/geometric data and or vtkXMLImageDataWriter for .vti volume rendering format and then as you said in your previous comment I can use vtk.js corresponding reader to reload the polydata or Image data on the JS side. I can do it.

But my project requirement is not to save them in a specific format and then pass that input on JS side.

I want to do the thing which you said here "I was saying that you should do the contour on the Python side and send the output of that filter to the client not the DICOM."

As the above code which I post yesterday, read the series of DICOM images and then with the help of ITK-TO-VTK converter, I received a vtkImageData and then with the help of vtkMarching cube filter I receive the final output in the form of vtkPolyData.

I also know that I can even apply the marching cube algorithm in client JS, bcoz I found this filter: vtk.Filters.General.vtkImageMarchingCubes.newInstance. So it’s no urgency to apply a marching cube filter on the server-side.

So finally I understood that I need to send vtkImageData or vtkPolyData from the server-side to the JS side for rendering. But I don’t know how can I send these objects to JS side. I tried with json.dump in server-side but I got errors as vtkCommonDataModelPython.vtkImageData’ object has no attribute ‘tolist’ or vtkFiltersCorePython.vtkMarchingCubes’ object has no attribute ‘tolist’.

I couldn’t understand how I am supposed to pass vtkPolyData or vtkImageData to JS. I check what is inside the vtkPolyData or vtkImageData in python vtk and I found below text:

vtkImageData:

vtkImageData (0x56351817f900)
  Debug: Off
  Modified Time: 109
  Reference Count: 3
  Registered Events: (none)
  Information: 0x5635202015d0
  Data Released: False
  Global Release Data: Off
  UpdateTime: 0
  Field Data:
    Debug: Off
    Modified Time: 72
    Reference Count: 1
    Registered Events: (none)
    Number Of Arrays: 0
    Number Of Components: 0
    Number Of Tuples: 0
  Number Of Points: 207822720
  Number Of Cells: 205777775
  Cell Data:
    Debug: Off
    Modified Time: 75
    Reference Count: 1
    Registered Events: 
      Registered Observers:
        vtkObserver (0x5635184304d0)
          Event: 33
          EventName: ModifiedEvent
          Command: 0x563518430870
          Priority: 0
          Tag: 1
    Number Of Arrays: 0
    Number Of Components: 0
    Number Of Tuples: 0
    Copy Tuple Flags: ( 1 1 1 1 1 0 1 1 1 1 1 )
    Interpolate Flags: ( 1 1 1 1 1 0 0 1 1 1 1 )
    Pass Through Flags: ( 1 1 1 1 1 1 1 1 1 1 1 )
    Scalars: (none)
    Vectors: (none)
    Normals: (none)
    TCoords: (none)
    Tensors: (none)
    GlobalIds: (none)
    PedigreeIds: (none)
    EdgeFlag: (none)
    Tangents: (none)
    RationalWeights: (none)
    HigherOrderDegrees: (none)
  Point Data:
    Debug: Off
    Modified Time: 108
    Reference Count: 1
    Registered Events: 
      Registered Observers:
        vtkObserver (0x563524bd3cc0)
          Event: 33
          EventName: ModifiedEvent
          Command: 0x563518430870
          Priority: 0
          Tag: 1
    Number Of Arrays: 1
    Array 0 name = nullptr
    Number Of Components: 1
    Number Of Tuples: 207822720
    Copy Tuple Flags: ( 1 1 1 1 1 0 1 1 1 1 1 )
    Interpolate Flags: ( 1 1 1 1 1 0 0 1 1 1 1 )
    Pass Through Flags: ( 1 1 1 1 1 1 1 1 1 1 1 )
    Scalars: 
      Debug: Off
      Modified Time: 104
      Reference Count: 1
      Registered Events: (none)
      Name: (none)
      Data type: unsigned char
      Size: 207822720
      MaxId: 207822719
      NumberOfComponents: 1
      Information: 0x5635184378b0
        Debug: Off
        Modified Time: 112
        Reference Count: 1
        Registered Events: (none)
        PER_COMPONENT: vtkInformationVector(0x5635183281d0)
      Name: (none)
      Number Of Components: 1
      Number Of Tuples: 207822720
      Size: 207822720
      MaxId: 207822719
      LookupTable: (none)
    Vectors: (none)
    Normals: (none)
    TCoords: (none)
    Tensors: (none)
    GlobalIds: (none)
    PedigreeIds: (none)
    EdgeFlag: (none)
    Tangents: (none)
    RationalWeights: (none)
    HigherOrderDegrees: (none)
  Bounds: 
    Xmin,Xmax: (0, 1315)
    Ymin,Ymax: (0, 1315)
    Zmin,Zmax: (0, 119)
  Compute Time: 127
  Spacing: (1, 1, 1)
  Origin: (0, 0, 0)
  Direction: (1, 0, 0, 0, 1, 0, 0, 0, 1)
  Dimensions: (1316, 1316, 120)
  Increments: (0, 0, 0)
  Extent: (0, 1315, 0, 1315, 0, 119)

vtkPolyData:

vtkMarchingCubes (0x563518155c10)
  Debug: Off
  Modified Time: 178
  Reference Count: 2
  Registered Events: (none)
  Executive: 0x563518434550
  ErrorCode: Success
  Information: 0x56351f4ad480
  AbortExecute: Off
  Progress: 0
  Progress Text: (None)
    Debug: Off
    Modified Time: 126
    Reference Count: 1
    Registered Events: (none)
    Contour Values: 
      Value 0: 83
  Compute Normals: On
  Compute Gradients: Off
  Compute Scalars: On
  Locator: (none)

I also went through PolyData | vtk.js link to know what should be the structure of vtkPloydata in Json file. I didn’t understand how to implement that.

I am having trouble passing vtkPolyData or vtkImageData from the server-side to the vtk.js side:
How to pass below iii to JS-side.

# Create your views here.
def IndexView(request):
    i = file_reader(directory)
    iso_Val = data_range(i)
    iii = contour(i, iso_Val)  # Creating vtkPloyData object
    # convert into JSON
    # iiii = iii.tolist()
    # reader = json.dumps(iiii)

    context = {"r": reader}
    return render(request, 'visapp/index.html', context)

Can you suggest to me how should I send the output of that filter to the client or what would be the process to pass these objects to the JS side?

Any help is appreciated :slightly_smiling_face:.

I don’t think the VTK team can tell you how you should write a web server and exchange data with your client over HTTP. This is beyond the scope of the VTK library. Also because it is a very common problem in Web development, you should be able to find many resources on that topic. Especially now that you’ve figured out the VTK part of it.