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

Add Hessian eigenvalues #29

Closed
wants to merge 43 commits into from
Closed

Add Hessian eigenvalues #29

wants to merge 43 commits into from

Conversation

hanslovsky
Copy link
Member

@hanslovsky hanslovsky commented Aug 8, 2016

This pull request features:

  • Computation of Hessian matrices (second derivatives)
  • Computation of eigenvalues for square and symmetric second order tensors in n-dimensional spaces.

For this purpose, these classes and methods were added:

  • Package net.imglib2.algorithm.corner:
    • RealComposite*Matrix: Implementation of apache RealMatrix as a View on imglib RealComposite to avoid unnecessary allocation and copy
    • HessianMatrix: Static methods to compute Hessian matrix from n-dimensional input.
    • TensorEigenValues: Static methods to compute tensor from n+1-dimensional input, where the last dimension stores linear representation of the tensor.
  • Package net.imglib2.algorithm.gradient:
    • PartialDerivative: Added static method gradientCentralDifferenceParallel to speed up gradient computation.

Before merging this, please comment on the package structure:

  • Should RealComposite*Matrix get their own package?
  • Should TensorEigenValues be in a package of its own, e.g. net.imglib2.algorithm.tensor?
  • Should the corner package be renamed?

Once the pull request is ready to be merged, I will rebase in my local repository and create a new pull request.

@hanslovsky
Copy link
Member Author

@tinevez's commit tinevez/CircleSkinner@84e4569 reminded me of this outstanding pull request. I should have some time next week to clean-up and simplify the interface. Specifically, I will change the interface such that all inputs must be supplied by the caller. That might be as extreme as having an interface that accepts only the gradient image. That way the number of overloads would be greatly reduced. Also, I would like to get rid of one of

func( args..., int nThreads, ExecutorService es)

and

func( args..., int nThreads )

Currently, I lean towards keeping the first option and removing the second one. Another option would be to get rid of the parallel computing in its entirety as this can be done by the caller by calculating the Hessian on Chunks of data. That would greatly reduce the number of overloads.

Thinking about the ImageJ picture, this should probably go into ops @ctrueden @dietzc . If you agree, I will write an op once this is merged in imglib2-algorithm. I will approach you with questions once I get there.

@dietzc
Copy link
Member

dietzc commented Feb 8, 2017

Hi @hanslovsky,

I'd keep the variant with ExecutorService which is a pattern used in other imglib2-algorithms, too (for example DoG). Having a wrapper in Ops would of course be great and I'm happy help to help you with that!

@tinevez
Copy link
Contributor

tinevez commented Feb 9, 2017

Hi @hanslovsky ,
Great news! I am sure that at the very least Hessian and partial derivatives will be a of great interest for people developing algorithms.

Here is a question regarding the Hessian calculation.
In the HessianMatrix class, you offer the possibility to specify a non-isotropic smoothing. This is great because then we accommodate e.g. 3D images for which the Z pixel size is different ant the X and Y pixel size (95% of confocal images).

But for the Hessian to be calculated correctly, the partial derivatives should be scaled accordingly too right?

@hanslovsky
Copy link
Member Author

@dietzc What I don't like about the ExecutorService pattern is that we have to somehow arbitrarily pick the number of tasks, e.g. as in DoG:

// FIXME find better heuristic?
final int numThreads = Runtime.getRuntime().availableProcessors();
final int numTasks = numThreads <= 1 ? 1 : numThreads * 20;

I will keep the ExecutorService pattern but add a nTasks parameter so the caller has to decide how many tasks he'd like to run.

@tinevez I will have to think about that!

@hanslovsky
Copy link
Member Author

hanslovsky commented Feb 22, 2017

@tinevez I think the appropriate way to get partial derivatives right is to re-sample the (smoothed) input image for isotropy. I would leave it to the caller to do that. What I add instead is a View that takes a Hessian matrix as input and scales the second derivatives according to a sigma parameter (the same you would use for smoothing), something like this:

public static < T extends RealType< T > > AbstractConvertedRandomAccessible< T, T > scaleHessian( final RandomAccessible< T > hessian, final T t, final double... sigma )
{
	final double minSigma = Arrays.stream( sigma ).reduce( Double.MAX_VALUE, ( d1, d2 ) -> Math.min( d1, d2 ) );
	final double minSigmaSq = minSigma * minSigma;
	final double[] scales = new double[ sigma.length * ( sigma.length + 1 ) / 2 ];
	for ( int i1 = 0, k = 0; i1 < sigma.length; ++i1 )
		for ( int i2 = i1; i2 < sigma.length; ++i2, ++k )
			scales[ k ] = sigma[ i1 ] * sigma[ i2 ] / minSigmaSq;
		final AbstractConvertedRandomAccessible< T, T > wrapped = new AbstractConvertedRandomAccessible< T, T >( hessian )
	{
		@Override
		public AbstractConvertedRandomAccess< T, T > randomAccess()
		{
			return new AbstractConvertedRandomAccess< T, T >( hessian.randomAccess() )
			{
				private final T tt = t;
				@Override
				public T get()
				{
					tt.set( source.get() );
					tt.mul( scales[ source.getIntPosition( sigma.length ) ] );
					return tt;
				}

				@Override
				public AbstractConvertedRandomAccess< T, T > copy()
				{
					// TODO Auto-generated method stub
					return null;
				}
			};
		}

		@Override
		public AbstractConvertedRandomAccess< T, T > randomAccess( final Interval interval )
		{
			return randomAccess();
		}
	};
	return wrapped;
}

Note that the scaling factors are normalized by the smallest sigma squared. The scaling has to be quadratic because the hessian matrix contains second derivatives. This should have minimal overhead and you could even write the contents of the view back into the hessianMatrix in a single pass without allocation of extra memory. Let me know what you think.

Non-symmetric matrices may have complex eigenvalues.  This interface change is thus required for compatibility with use cases other than Hessian matrix (symmetric) eigenvalues.
…ompute

Now, matrix based EigenValue implementations hold a RealCompositeMatrix object with exchangable data Composite instead of creating an instance at each call of compute.  As a consequence EigenValues are not thread safe in general and a copy method was added to the interface.
Replace the checks by asserts and if the caller has input that might be out of bounds the check should be done outside getEntry.
…rom RealComposite to Composite.

We do not care about the kind of composite that is passed.  We only need to call Composite.get.
@hanslovsky
Copy link
Member Author

I added a convenience method to scale the hessian matrix based on a sigma[] parameter passed by the caller (cf the last commit @tinevez ). I also added the missing license headers (how did I miss them?)

Is this ready to be merged now?

@tinevez
Copy link
Contributor

tinevez commented Apr 3, 2017

I could use this PR, once merged, in imagej-ops to port the tubenessop.

@hanslovsky
Copy link
Member Author

hanslovsky commented Apr 24, 2017

ojAlgo v43 was just released: optimatika/ojAlgo@5487fa8
I will update the tensor eigenvalues and matrices to use ojAlgo as backend instead of commons-math and then we should be able to merge this (after further review).

Update: Once this is confirmed for a merge I will make a rebase into a single commit.

hanslovsky and others added 2 commits April 24, 2017 13:44
Eclipse (neon.2) seems to have issues indenting parameters following generic wildcards.
@axtimwalde
Copy link
Member

@/all Is this ready for a merge?

@hanslovsky
Copy link
Member Author

It is from my side (after a rebase, of course).

@tpietzsch
Copy link
Member

I'm looking at it now

Copy link
Member

@tpietzsch tpietzsch left a comment

Choose a reason for hiding this comment

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

I left a few inline suggestions/comments, but from my side this is in principle ready for merging.

Cool that you are using ojAlgo.

In general, I would be interested in the trade-offs, with respect to performance and expressiveness, of Vector/Matrix NativeTypes vs. the Composite approach. Unfortunately I have no time to look into it now...

final int nDim = source.numDimensions();
if ( nDim < 2 )
{
gradientCentralDifference2( source, gradient, dimension );
Copy link
Member

Choose a reason for hiding this comment

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

Why are you calling gradientCentralDifference2, not gradientCentralDifference in this case?
Is it faster? Did you test?

Copy link
Member Author

Choose a reason for hiding this comment

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

No particular reason: I will change that to gradientCentralDifference to be consistent with what happens in HessianMatrix and use the faster version (according to the comments in PartialDerivative.

assert sigma.length * ( sigma.length + 1 ) / 2 == hessian.dimension( sigma.length );
final int maxD = sigma.length;

final double minSigma = Arrays.stream( sigma ).reduce( Double.MAX_VALUE, ( d1, d2 ) -> Math.min( d1, d2 ) );
Copy link
Member

Choose a reason for hiding this comment

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

final double minSigma = Util.min( sigma ) would be more readable. (net.imglib2.util.Util)

return hessianMatrix;
}

public static < T extends RealType< T > > IntervalView< T > scaleHessianMatrix( final RandomAccessibleInterval< T > hessian, final double[] sigma )
Copy link
Member

Choose a reason for hiding this comment

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

Can you add javadoc? It's not obvious what this method does

this( data, nRows, nCols, nRows * nCols );
}

public RealCompositeMatrix( final Composite< T > data, final int nRows, final int nCols, final int length )
Copy link
Member

Choose a reason for hiding this comment

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

I would make this constructor (and all constructors of derived classes having a length argument) protected. The constructor only makes sense for being able to derive classes. In all other scenarios the length element is redundant.

expectedLength() can be removed then.

* @author Philipp Hanslovsky
*
*/
public class HessianMatrix
Copy link
Member

Choose a reason for hiding this comment

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

I wonder whether it is worth adding a version that takes gaussian and gradient arguments that have appropriate size so that an OutOfBounds factory is not required. This would be useful if you want to do the Hessian computation blockwise to avoid artifacts at block boundaries. Of course, this can also be added later when it is needed.

Copy link
Member Author

Choose a reason for hiding this comment

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

I have not thought about that but I like the idea. I did not like the extra OufOfBounds so we should keep that in our minds. I have only one concern which actually might be a non-issue: If you already have the gradients pre-computed, you can just call the overload takes only the gradients as input. If you do not have them pre-computed, you will probably want to re-use them as features elsewhere and you might want to have them at the same size as the input.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I fully agree

* n+1-dimensional {@link RandomAccessibleInterval} for storing
* the gradients along all axes of the smoothed source (size of
* last dimension is n)
* @param hessianMatrix
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a javadoc description of the upper triangular layout of the hessianMatrix (can be adapted from TensorEigenValues javadoc)?

{
// TODO Auto-generated catch block
e.printStackTrace();
}
Copy link
Member

Choose a reason for hiding this comment

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

e.printStackTrace() is probably not what should happen here.
This is a problem also in other algorithms (e.g., DifferenceOfGaussian, SeparableSymmetricConvolution, ...).
So I would merge this anyway and open an issue to fix it in all places.

For InterruptedException the right thing to do would be to restore the interruption status of the current thread and return.

For ExecutionException I don't know ...

Copy link
Member

Choose a reason for hiding this comment

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

#40

Quoting @tpietzsch
"The constructor only makes sense for being able to derive classes. In all other scenarios the length element is redundant.

expectedLength() can be removed then."
@tpietzsch rightfully mentioned that it was not clear what scaleHessianMatrix does.  Added some JavaDoc for clarification.
@hanslovsky
Copy link
Member Author

#41 is the same pull request but from a rebased branch to have a single commit.

@hanslovsky hanslovsky closed this May 16, 2017
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.

5 participants