Skip to content
Snippets Groups Projects
CCCT.java 9.23 KiB
/*-
 * #%L
 * Fiji distribution of ImageJ for the life sciences.
 * %%
 * Copyright (C) 2015 - 2022 Fiji developers.
 * %%
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public
 * License along with this program.  If not, see
 * <http://www.gnu.org/licenses/gpl-3.0.html>.
 * #L%
 */
package fr.pasteur.iah.ccct;

import static fr.pasteur.iah.ccct.trackmate.CellContactDetectorFactory.KEY_CHANNEL_1;
import static fr.pasteur.iah.ccct.trackmate.CellContactDetectorFactory.KEY_CHANNEL_2;
import static fr.pasteur.iah.ccct.trackmate.CellContactDetectorFactory.KEY_CONTACT_SENSITIVITY;
import static fr.pasteur.iah.ccct.trackmate.CellContactDetectorFactory.KEY_SIGMA_FILTER;
import static fr.pasteur.iah.ccct.trackmate.CellContactDetectorFactory.KEY_THRESHOLD_1;

import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;

import javax.swing.ImageIcon;

import org.scijava.util.VersionUtils;

import fiji.plugin.trackmate.gui.GuiUtils;
import fiji.plugin.trackmate.util.TMUtils;
import fr.pasteur.iah.ccct.util.Thresholder;
import ij.CompositeImage;
import ij.IJ;
import ij.ImagePlus;
import ij.WindowManager;
import ij.plugin.PlugIn;
import net.imagej.ImgPlus;
import net.imagej.axis.Axes;
import net.imglib2.IterableInterval;
import net.imglib2.RandomAccessibleInterval;
import net.imglib2.algorithm.MultiThreaded;
import net.imglib2.algorithm.labeling.ConnectedComponentAnalysis;
import net.imglib2.algorithm.neighborhood.CenteredRectangleShape;
import net.imglib2.img.Img;
import net.imglib2.img.display.imagej.ImageJFunctions;
import net.imglib2.img.planar.PlanarImgFactory;
import net.imglib2.multithreading.SimpleMultiThreading;
import net.imglib2.type.NativeType;
import net.imglib2.type.logic.BitType;
import net.imglib2.type.numeric.RealType;
import net.imglib2.type.numeric.integer.UnsignedShortType;
import net.imglib2.util.Util;
import net.imglib2.view.IntervalView;
import net.imglib2.view.Views;

@SuppressWarnings( "deprecation" )
public class CCCT< T extends RealType< T > & NativeType< T > > implements PlugIn, CCCTProcessor, MultiThreaded
{

	public static final String PLUGIN_VERSION = VersionUtils.getVersion( CCCT.class );
	public static final String PLUGIN_NAME = "CCCT";

	public static final ImageIcon ICON = new ImageIcon( CCCT.class.getResource( "LogoCellContactAnalyzer-128x128.png" ) );;

	public static final ImageIcon ICON_SMALL = new ImageIcon( CCCT.class.getResource( "LogoCellContactAnalyzer-64x64.png" ) );;

	ImagePlus imp;

	private CCCTGui gui;

	private int numThreads;

	public CCCT()
	{
		setNumThreads( 10 );
	}

	@Override
	public void run( final String arg )
	{
		if ( null != arg && arg.length() > 0 )
		{
			imp = new CompositeImage( new ImagePlus( arg ) );
		}
		else
		{
			imp = WindowManager.getCurrentImage();
		}
		if ( null == imp )
		{
			IJ.error( PLUGIN_NAME + " v" + PLUGIN_VERSION, "Please open a multi-channel image first." );
			return;
		}
		if ( !imp.isVisible() )
		{
			imp.show();
		}

		gui = new CCCTGui( imp, this );
		GuiUtils.positionWindow( gui, imp.getWindow() );
		gui.setVisible( true );
	}

	@Override
	public void process( final boolean showContactImage, final boolean contactMask, final boolean contactLabels, final boolean trackLabels )
	{
		@SuppressWarnings( "unchecked" )
		final ImgPlus< T > img = TMUtils.rawWraps( imp );

		final Map< String, Object > settings = gui.getSettings();

		// In ImgLib2, dimensions are 0-based.
		final int channel1 = ( Integer ) settings.get( KEY_CHANNEL_1 ) - 1;
		final int channel2 = ( Integer ) settings.get( KEY_CHANNEL_2 ) - 1;
		final int contactSensitivity = ( Integer ) settings.get( KEY_CONTACT_SENSITIVITY );
		final double sigma = ( Double ) settings.get( KEY_SIGMA_FILTER );
		final double thresholdC1 = ( Double ) settings.get( KEY_THRESHOLD_1 );
		final double thresholdC2 = ( Double ) settings.get( KEY_THRESHOLD_1 );

		final RandomAccessibleInterval< T > im1;
		final RandomAccessibleInterval< T > im2;

		final int cDim = img.dimensionIndex( Axes.CHANNEL );
		if ( cDim < 0 )
		{
			im1 = img;
			im2 = img;
		}
		else
		{
			im1 = Views.hyperSlice( img, cDim, channel1 );
			im2 = Views.hyperSlice( img, cDim, channel2 );
		}

		final PlanarImgFactory< T > factory = new PlanarImgFactory<>( Util.getTypeFromInterval( img ) );
		final Img< T > out = factory.create( im1 );

		int timeDim = img.dimensionIndex( Axes.TIME );
		if ( timeDim >= 0 )
			if ( cDim >= 0 && timeDim > cDim )
				timeDim--;

		final int td = timeDim;
		final int nFrames = imp.getNFrames();

		{
			gui.setProgressStatus( "Contact image" );
			final AtomicInteger ai = new AtomicInteger( 0 );
			final Thread[] threads = SimpleMultiThreading.newThreads( numThreads );
			for ( int i = 0; i < threads.length; i++ )
			{
				threads[ i ] = new Thread( "CCCT Thread Contact Image" )
				{
					@Override
					public void run()
					{
						for ( int frame = ai.getAndIncrement(); frame < nFrames; frame = ai.getAndIncrement() )
						{
							final ContactImgGenerator< T > algo = new ContactImgGenerator< T >(
									Views.hyperSlice( im1, td, frame ),
									Views.hyperSlice( im2, td, frame ),
									Views.hyperSlice( out, out.numDimensions() - 1, frame ),
									thresholdC1, thresholdC2, contactSensitivity, sigma );

							if ( !algo.checkInput() || !algo.process() )
							{
								System.err.println( algo.getErrorMessage() );
								return;
							}

							gui.setProgress( ( 1.0 + ai.get() ) / nFrames );
						}
					}
				};
			}
			SimpleMultiThreading.startAndJoin( threads );

			if ( showContactImage )
			{
				final ImagePlus contacts = ImageJFunctions.wrap( out, "Contacts" );
				contacts.setCalibration( imp.getCalibration().copy() );
				contacts.setDimensions( 1, imp.getNSlices(), imp.getNFrames() );
				contacts.show();
				contacts.resetDisplayRange();
				IJ.run( "Fire" );
			}
		}
		/*
		 * Generate binary mask.
		 */

		if ( contactMask || contactLabels )
		{
			final PlanarImgFactory< BitType > maskFactory = new PlanarImgFactory<>( new BitType() );
			final Img< BitType > mask = maskFactory.create( im1 );
			final T valTreshold = out.firstElement().createVariable();
			valTreshold.setZero();

			gui.setProgressStatus( "Contact masks" );
			final AtomicInteger ai = new AtomicInteger( 0 );
			final Thread[] threads = SimpleMultiThreading.newThreads( numThreads );
			for ( int i = 0; i < threads.length; i++ )
			{
				threads[ i ] = new Thread( "CCCT Thread Contact Mask" )
				{
					@Override
					public void run()
					{
						for ( int frame = ai.getAndIncrement(); frame < nFrames; frame = ai.getAndIncrement() )
						{
							final IterableInterval< BitType > maskSlice = Views.hyperSlice( mask, mask.numDimensions() - 1, frame );
							final IntervalView< T > slice = Views.hyperSlice( out, out.numDimensions() - 1, frame );
							Thresholder.threshold( slice, maskSlice, valTreshold, true, 1 );
							gui.setProgress( ( 1.0 + ai.get() ) / nFrames );
						}
					}
				};
			}
			SimpleMultiThreading.startAndJoin( threads );

			if ( contactMask )
			{
				final ImagePlus masks = ImageJFunctions.wrap( mask, "ContactMasks" );
				masks.setCalibration( imp.getCalibration() );
				masks.setDimensions( 1, imp.getNSlices(), imp.getNFrames() );
				masks.show();
			}

			if ( contactLabels )
			{
				gui.setProgressStatus( "Contact labels" );
				gui.setProgress( 0. );
				final Img< UnsignedShortType > lbl = Util.getArrayOrCellImgFactory( mask, new UnsignedShortType() ).create( mask );

				if ( trackLabels && imp.getNFrames() > 1 )
					/*
					 * Connect in all dimensions, including time. This will
					 * effectively track contacts, provided they overlap from
					 * one frame to another.
					 */
					ConnectedComponentAnalysis.connectedComponents( mask, lbl );
				else
				{
					/*
					 * Skip the time dimension when connecting mask.
					 */
					final int numDimensions = mask.numDimensions();
					final int[] span = new int[ numDimensions ];
					Arrays.fill( span, 1 );
					if ( imp.getNSlices() > 1 )
						span[ 3 ] = 0;
					else
						span[ 2 ] = 0;

					ConnectedComponentAnalysis.connectedComponents( mask, lbl, new CenteredRectangleShape( span, false ) );
				}

				// For display range.
				int maxLbl = -1;
				for ( final UnsignedShortType pixel : lbl )
					if ( pixel.get() > maxLbl )
						maxLbl = pixel.get();

				final ImagePlus labels = ImageJFunctions.wrap( lbl, "ContactLabels" );
				labels.setCalibration( imp.getCalibration().copy() );
				labels.setDimensions( 1, imp.getNSlices(), imp.getNFrames() );
				labels.setDisplayRange( 0, maxLbl );
				labels.show();
				IJ.run( "3-3-2 RGB" );
			}
		}
	}

	@Override
	public void setNumThreads()
	{
		this.numThreads = Runtime.getRuntime().availableProcessors();
	}

	@Override
	public void setNumThreads( final int numThreads )
	{
		this.numThreads = numThreads;
	}

	@Override
	public int getNumThreads()
	{
		return numThreads;
	}
}