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

Implementation of variance reconstruction #404

Open
wants to merge 2 commits into
base: master
Choose a base branch
from

Conversation

jannisdickmann
Copy link

Implementation of variance reconstruction with a new FFTVarianceRampImageFilter and modifications to FFTRampImageFilter and FDKWeightProjectionFilter.

…mageFilter and modifications to FFTRampImageFilter and FDKWeightProjectionFilter.
@SimonRit
Copy link
Collaborator

SimonRit commented Mar 2, 2021

Thanks Jannis. I would like to add a test and I can try to implement a functional one. If I want to reconstruct the variance, should I compute variance projections, then FDKWeightProjectionFilter (VarianceMode) -> FFTVarianceRampImageFilter -> FDKBackProjectionImageFilter ?

@jannisdickmann
Copy link
Author

Hi Simon. Yes that's correct. In the FFTVarianceRampImageFilter we should probably also test for the correct calculation of finterp, which should be 0.46 without Hann filter, and I can provide expected values for other Hann values. The HannY option is currently not considered and I was just printing an error message, which probably is not ideal.

{
idxshifted = itiK.GetIndex();
if(idxshifted[0] == 0)
idxshifted[0] = width-1;
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using width can actually cause problems, since the inverse Fourier of the Fourier transform is not guaranteed to have the size (see https://itk.org/Doxygen/html/classitk_1_1HalfHermitianToRealInverseFFTImageFilter.html: "the input is assumed to consist of roughly half the full complex image resulting from a real-to-complex discrete Fourier transform").
One solution would be to query KernelIFFT directly for its length. The other solution may be to use SetActualDimensionIsOdd() on the FFT filter (https://itk.org/Doxygen/html/classitk_1_1HalfHermitianToRealInverseFFTImageFilter.html#af072dcf520dee228202a11b6e99597f5) although I would have to test how this works.
Do you have any preference?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No preference, no. Like you, I don't know what are the cons of using SetActualDimensionIsOdd(). Pick the solution you prefer!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a new commit where I determine the size of the Fourier-transformed filter again instead of relying on the width variable. This seems to me the safest option.

Copy link
Collaborator

@SimonRit SimonRit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry for the delay. I have a few questions... Can you answer and I can implement the changes myself depending on your answers? Thanks in advance.

Comment on lines +63 to +67
if(this->m_VarianceMode)
{
// square if in variance mode
m_ConstantProjectionFactor[k] = m_ConstantProjectionFactor[k] * m_ConstantProjectionFactor[k];
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm surprised you only square the constant part of the weight. Has this been tested with a divergent geometry? For simplicity, my suggestion would be to apply this filter twice instead of adding a parameters.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed we have almost always used parallel geometries. We have also tested it with a divergent geometry and errors were few percent, but those could of course increase if the SID is not 2m as for pCT, but smaller. I like the idea of applying the filter twice. This would also avoid the special case with m_VarianceMode. However, there should be some note in the rtkFFTVarianceRampImageFilter.h that the weights filter needs to be applied twice.

Comment on lines +131 to +133
/** Set/Get the previous kernel update size */
itkGetConstMacro(PreviousKernelUpdateSize, SizeType);
itkSetMacro(PreviousKernelUpdateSize, SizeType);
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be protected? Any reason to expose this parameter to the user?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Setting the variable protected is better. I don't see a reason why it needs to be exposed. It just needs to be accessible here.

public rtk::FFTRampImageFilter<TInputImage, TOutputImage, TFFTPrecision>
{
public:
typedef rtk::FFTRampImageFilter<TInputImage, TOutputImage, TFFTPrecision> Baseclass;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is Superclass in ITK, should be removed

Comment on lines +128 to +179
// iFFT kernel
using IFFTType = itk::HalfHermitianToRealInverseFFTImageFilter< FFTOutputImageType, FFTInputImageType >;
typename IFFTType::Pointer ifftK = IFFTType::New();
ifftK->SetInput( this->m_KernelFFT );
ifftK->SetNumberOfThreads( this->GetNumberOfThreads() );
ifftK->Update();
typename FFTType::InputImageType::Pointer KernelIFFT = ifftK->GetOutput();

// calculate ratio g_C/g^2
using InverseIteratorType = itk::ImageRegionIteratorWithIndex<typename FFTType::InputImageType>;
InverseIteratorType itiK(KernelIFFT, KernelIFFT->GetLargestPossibleRegion());
typename FFTType::InputImageType::PixelType sumgc = 0.;
typename FFTType::InputImageType::PixelType sumg2 = 0.;
typename FFTType::InputImageType::IndexType idxshifted;
unsigned int const widthFFT = KernelIFFT->GetLargestPossibleRegion().GetSize()[0];
for(; !itiK.IsAtEnd(); ++itiK)
{
idxshifted = itiK.GetIndex();
if(idxshifted[0] == 0)
idxshifted[0] = widthFFT-1;
else
idxshifted[0] -= 1;

sumgc += itiK.Get() * KernelIFFT->GetPixel(idxshifted);
sumg2 += itiK.Get() * itiK.Get();
}
typename FFTType::InputImageType::PixelType const ratiogcg2 = sumgc/sumg2;

// numerical integration to calculate f_interp
double const aprecision = 0.00001;
double finterp = 0.;
for(unsigned int i=0; i<int(1./aprecision); i++)
{
double const a = double(i)*aprecision;
finterp += (1-a)*(1-a) + 2*ratiogcg2*(1-a)*a + a*a;
}
finterp *= aprecision;

// square kernel and multiply with finterp
itiK.GoToBegin();
for(; !itiK.IsAtEnd(); ++itiK)
{
itiK.Set(itiK.Get() * itiK.Get() * finterp);
}

// FFT kernel
typename FFTType::Pointer fftK2 = FFTType::New();
fftK2->SetInput( ifftK->GetOutput() );
fftK2->SetNumberOfThreads( this->GetNumberOfThreads() );
fftK2->Update();
this->m_KernelFFT = fftK2->GetOutput();

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This part is the only difference. Maybe we should consider putting this in a function called by the parent, which would be empty for the parent and redefined for this class? Then the reimplementation of UpdateFFTProjectionsConvolutionKernel can be removed as well as the getter and setter for PreviousKernelUpdateSize.

I also wonder, could this have been done after the application of the windowing and finterp kept to the original value without the numerical integration?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the first part, I totally agree. It's nicer to just implement the part that actually changes. Splitting UpdateFFTProjectionsConvolutionKernel of rtkFFTRampImageFilter.h into several steps would also make it more flexible for other RTK users that just want to modfiy one component of the filter.

I'm not sure if I understood the second part right, but I'll try to motivate what I did here.

To calculate finterp one needs access to the filter including the windowing function in real space. finterp is then an integral that depends on the filters g^2 and g_C, which in turn depend on the windowed filter in real space. The implementation that I propose is computationally most expensive, but also most flexible, since it can take any windowed ramp filter. Alternatively I could fit an function finterp(Hann) and avoid the numerical integration and going back and forth between Fourier and real space.

Right now the code does the following:

  • define ramp in real space
  • go to Fourier space
  • apply window function
  • go back to real space
  • calculate finterp
  • square filter and multiply by finterp
  • go to Fourier space

I am not sure if the many times going back and forth between real space and Fourier space can cause problems (like the filter size change I fixed in this commit). Maybe it can be avoided by re-defining the squared ramp in real space?

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

Successfully merging this pull request may close these issues.

2 participants