Moving data from VTK to ITK: issue in memory management

Hello everyone,
I was advised to post this topic here. But the initial issue is here: Moving data from VTK to ITK: issue in memory management - Support - VTK

I use ITK (5.4.4) and VTK (9.5.2) in my code. I use some VTK classes (like vtkImageStencil) which use some mesh (vtkPolyData) and i want the result in an ITK image.
To convert result of the vtkImageStencil in an ITK image, i use VTKImageToImageFilter.
When i get the output of the VTKImageToImageFilter in my itk image, it seems that the data buffer is still managed by the vtkImageStencil. Indeed when i delete the vtkImageStencil and after try to access the data buffer of my itk image, the application crash.
I try to duplicate the output of the VTKImageToImageFilter, before filling my ITK Image, it seems to fix my issue.
I see nowhere in the documentation that the user must copy the buffer/the Stencil keep the control of the output buffer.

Am i using the right classes in the right way ?
Is this a lack of detail in the documentation, or a bug in the expected behavior of the VTK classes?

Thank

Here a minimal sample:

#include “itkImage.h”
#include “itkVTKImageToImageFilter.h”

#include <vtkNew.h>
#include <vtkImageStencil.h>
#include <vtkPolyDataToImageStencil.h>
#include <vtkPointData.h>
#include <vtkPolyData.h>
#include <vtkDataArray.h>
#include <vtkImageData.h>
#include <vtkSphereSource.h>
#include        // juste use for qDebug
#include “itkMinimumMaximumImageCalculator.h”

class Voxelisation
{
public:


Voxelisation() = default;
\~Voxelisation() = default;

void setInputData(vtkPolyData\* p_inputData);

void execute(itk::Image<unsigned char, 3>::Pointer& p_outputImage);

std::array<double, 3> m_spacing {0.5, 0.5, 0.5};

unsigned char m_backgroundValue {0};

unsigned char m_foregroundValue {255};

vtkNew<vtkImageStencil> m_vtkImageStencil;

};

void Voxelisation::setInputData(vtkPolyData\* p_inputData)
{
vtkNew whiteImage;
std::array<double, 3> origin;
{
std::array<double, 6> bounds;
p_inputData->GetBounds(bounds.data());
whiteImage->SetSpacing(m_spacing.data());
std::array<int, 3> dim;
for (size_t i = 0; i < 3; i++)
{
dim\[i\] = static_cast(ceil((bounds\[i \* 2 + 1\] - bounds\[i \* 2\]) / m_spacing\[i\]));
}
whiteImage->SetDimensions(dim.data());
whiteImage->SetExtent(0, dim\[0\] - 1, 0, dim\[1\] - 1, 0, dim\[2\] - 1);
origin\[0\] = bounds\[0\] + m_spacing\[0\] / 2;
origin\[1\] = bounds\[2\] + m_spacing\[1\] / 2;
origin\[2\] = bounds\[4\] + m_spacing\[2\] / 2;
whiteImage->SetOrigin(origin.data());
whiteImage->AllocateScalars(VTK_UNSIGNED_CHAR, 1);
const vtkIdType count = whiteImage->GetNumberOfPoints();
for (vtkIdType i = 0; i < count; ++i)
{
whiteImage->GetPointData()->GetScalars()->SetTuple1(i, m_foregroundValue);
}
}
vtkNew vtkPolyDataToImageStencil;
{
vtkPolyDataToImageStencil->SetInputData(p_inputData);
vtkPolyDataToImageStencil->SetOutputOrigin(origin.data());
vtkPolyDataToImageStencil->SetOutputSpacing(m_spacing.data());
vtkPolyDataToImageStencil->SetOutputWholeExtent(whiteImage->GetExtent());
vtkPolyDataToImageStencil->Update();
}

m_vtkImageStencil->SetInputData(whiteImage);
m_vtkImageStencil->SetStencilConnection(vtkPolyDataToImageStencil->GetOutputPort());
m_vtkImageStencil->ReverseStencilOff();
m_vtkImageStencil->SetBackgroundValue(m_backgroundValue);

}

void Voxelisation::execute(itk::Image<unsigned char, 3>::Pointer& p_outputImage)
{
m_vtkImageStencil->Update();
auto VTKImageToImageFilter = itk::VTKImageToImageFilter<itk::Image<unsigned char, 3>>::New();
VTKImageToImageFilter->SetInput(m_vtkImageStencil->GetOutput());
VTKImageToImageFilter->UpdateLargestPossibleRegion();

qDebug() << "refCount inside Voxelisation" << VTKImageToImageFilter->GetOutput()->GetReferenceCount();      //1

p_outputImage = VTKImageToImageFilter->GetOutput();
qDebug() << "refCount p_outputImage inside Voxelisation" << p_outputImage->GetReferenceCount(); // 2
if( p_outputImage == nullptr)
{
    qDebug() << "outputImage is nullptr";
}
else {
    qDebug() << "outputImage is not null"; // not null
}

}

int main(int argc, char \*argv)
{
itk::Image<unsigned char, 3>::Pointer outputImage = nullptr;
vtkNew mesh;

//Fill the Mesh with a Sphre inside
vtkNew<vtkSphereSource> sphereSource;
sphereSource->SetPhiResolution(21);
sphereSource->SetThetaResolution(21);
sphereSource->SetRadius(20);
sphereSource->SetCenter(0,0,0);
sphereSource->SetOutput(mesh);
sphereSource->Update();

Voxelisation voxelAlgo;
voxelAlgo.setInputData(mesh);
voxelAlgo.execute(outputImage);

if(outputImage == nullptr)
{
    qDebug() << "outputImage == nullptr";
    return 1;
}

//Check refcount of the output image
qDebug() << "ref count of the output image" << outputImage->GetReferenceCount(); //1

//check some value inside the output image
{
    auto imageCalculatorFilter = itk::MinimumMaximumImageCalculator<itk::Image<unsigned char, 3>>::New();
    imageCalculatorFilter->SetImage(outputImage);
    imageCalculatorFilter->SetRegion(outputImage->GetLargestPossibleRegion());
    imageCalculatorFilter->ComputeMaximum();
    const unsigned char maximum = imageCalculatorFilter->GetMaximum();
    qDebug() << "maximum before delete" << maximum; // value is 255
}

// now delete the stencil object
voxelAlgo.m_vtkImageStencil->Delete();

//Check refcount of the output image again
// must be the same
qDebug() << "ref count of the output image again" << outputImage->GetReferenceCount(); //1

//check some value inside the output image again
{
    auto imageCalculatorFilter = itk::MinimumMaximumImageCalculator<itk::Image<unsigned char, 3>>::New();
    imageCalculatorFilter->SetImage(outputImage);
    imageCalculatorFilter->SetRegion(outputImage->GetLargestPossibleRegion());
    qDebug() << "before recalcul the maximum after delete";
    imageCalculatorFilter->ComputeMaximum();        //CRASH here
    qDebug() << "Crash just before";
    const unsigned char maximum = imageCalculatorFilter->GetMaximum();
    qDebug() << "maximum after delete" << maximum;
}

}

Result of this code is:
refCount inside Voxelisation 1
refCount p_outputImage inside Voxelisation 2
outputImage is not null
ref count of the output image 1
maximum before delete 255
ref count of the output image again 1
before recalcul the maximum after delete

Does the problem occur if you let the stencil object go out of scope instead of manually calling delete on it? Have you considered difference between Delete and FastDelete?

If i did not delete the object Voxelisation (which old the stencil): everything work find. But it’s normal i did’not try to access the content of the itk image after delete of the stencil, which is my issue.

I try to use FastDelete instead of Delete on my stencil.
The application did’nt crash when try to access to the content of the ITK image after the destruction of the stencil.
But i have an assertion at the end of the application:
”Assertion failed: e->Component->NetCount > 0, file C:\Dev\vtk\vtk\Common\Core\vtkGarbageCollector.cxx, line 754”

I guess the issue is that VTK owns the image buffer, and there needs to be vtkSmartPointer reference to it, for it to not be cleaned up.

So you could duplicate the buffer into an ITK-owned image. Or add an extra vtkSmartPointer reference to the underlying buffer, and release it when image is no longer needed. Release by letting the variable go out of scope, or explicitly assign null pointer to it.

@matt.mccormick and @blowekamp Is this right? Or is VTK to ITK converter supposed to hold this reference?