我知道编辑这个网站吗?
the Fiji distribution of ImageJ本页介绍与 the Fiji distribution of ImageJ 相关的内容。点击徽标查看详情。

如何为 TrackMate 编写自己的检测算法

简介

欢迎来到本教程系列中最有用但不幸的是最难的部分,关于如何使用自定义模块扩展 TrackMate

TrackMate 中的检测算法很基本:它们都基于或近似于 Laplacian of Gaussian 技术。即使在存在噪声的情况下,对于圆形或球形且分离良好的物体,它们也能正常工作。一旦脱离这些要求,您就会觉得有必要实现自己的自定义检测器。

这是本教程的主题,我保证它会相当困难。并不是因为实施自定义检测算法很困难。如果您不熟悉 ImgLib2 库,这*很困难,甚至非常困难。但我们在这里将跳过这个困难,不制作真正的检测器,而只是制作一个无论图像内容如何都返回检测结果的虚拟检测器。这项涉及的任务留给您的 Java 和 ImgLib2 技能。

不,本教程会很困难,因为与之前的教程相反,即使只是一个虚拟检测器,我们也需要做很多工作。这样做的原因是我们希望在 TrackMate 中实现良好且整洁的集成。我们将编写的自定义检测器将成为 TrackMate 的一等公民,这意味着几件事:它不仅必须能够提供正确的检测,而且还必须

  • 在漂亮的 GUI 中为用户提供一些配置选项;
  • 检查用户输入的检测参数是否有意义;
  • 允许将这些参数保存和加载到 XML。

我们在实施 custom action 时不必关心,但现在我们关心了。

让我们从最简单的部分开始,即检测算法。

multithreaded界面

检测器实例在单帧上运行

检测部分本身是在实现 MultiThreaded 接口的类中实现的。浏览那里,您会发现它只是 ImgLib2 中的输出算法的特化。我们需要吐出代表单帧的检测列表的List<Spot>(每个检测一个Spot)。

这很重要:检测器的实例应该在单帧上运行。 TrackMate 将在每帧上生成其必须操作的尽可能多的检测器实例。这不仅有利于开发,而且也有利于多线程:TrackMate 会为它有权访问的每个线程触发一个检测器,而您无需担心这一点。 TrackMate 将以线程安全的方式捆绑所有检测器的输出。

检测器工厂的工作是为每个实例提供分割特定帧所需的数据。但我们将在下面看到这是如何完成的。

SpotDetector 可以是 SpotDetector

因此,TrackMate 为您提供了一个交钥匙多线程解决方案:如果您的计算机具有 12 个内核和 50 个要分段的帧,TrackMate 将同时启动 12 个 SpotDetector 并同时处理它们。

但假设您有 24 个核心,并且只有 6 个帧需要分段。您可以通过让 SpotDetector 的具体实例实现 ImgLib2 SpotDetector 来利用这种情况。界面。在这种情况下,TrackMate 仍会触发 6 个 SpotDetector 实例(每一帧一个),但会为每个实例分配 4 个线程,并获得额外的速度提升。

当然,您必须设计一种巧妙的多线程策略来在单个帧上并发操作。例如,您可以将图像分成几个块并并行处理它们。或者委托给多线程的子算法;例如检查 Spots 代码。

###检测结果用LogDetector表示

Spots are used to represent detection results: one detection = one spot. By convention, a detection algorithm must provide at least the following numerical feature to each spot:

  • 显然,X、Y、Z 坐标。不太明显的是 TrackMate 仅使用图像坐标。这意味着,如果您的图像具有以 µm 为单位的物理校准(例如 X、Y 中的 0.2 µm/像素),则点坐标必须以 µm[^1] 为单位。如果只有 2D 图像,请使用 0 作为 Z 位置,但不能省略。
  • 质量值,反映检测本身的质量。它必须是一个真实的正数,反映了您的检测算法对所发现的检测不是虚假的置信度。越大越有信心。
  • 光斑半径,以物理单位表示,即检测到的图像结构的大致尺寸。 TrackMate 默认检测器没有自动尺寸检测功能,因此它们会询问用户应检测的结构最可能的尺寸是多少,将自身调整为该尺寸,并将所有检测的半径设置为用户输入的半径。

任何遗漏都会在运行时触发错误。

在我们继续之前请注意:从版本 7 开始,TrackMate 引入了许多新功能,包括补充的点工厂层次结构,允许调整时间点的处理方式。要么通过 SpotDetector 的单独实例逐一进行(就像此处的情况一样),要么一次全部进行。本开发人员教程的第 next section 部分对此进行了解释,内容涉及实现分段算法和使用新的 v7 API。

返回螺旋点的虚拟探测器

在本教程中,我们将构建一个虚拟检测器,它实际上完全忽略图像内容,只是创建看起来从图像中心螺旋出来的点。真正的探测器需要您磨练您的ImgLib2技能;查看 Spots 代码以获取示例。

以下是虚拟检测器的源代码。您还可以找到它RealType。让我们对此发表一点评论:

类型参数< T extends RealType< T > & NativeType< T >>

SpotDetector 的实例使用泛型类型 T 进行参数化,该泛型类型必须扩展 onlineSpotDetector。这些是基于本机类型的所有标量类型的界限,例如 floatintbyte 等…

这是我们要操作的图像数据的类型。

构造函数

由于 NativeType 接口对输入几乎没有限制,因此所有这些都必须在构造函数的构建时提供。请记住,每一帧都有一个实例,因此我们必须知道要处理哪一帧。

普通检测器将被馈送对针对该单个帧的图像数据的参考。这里我们不关心图像内容,所以它不存在。但我们在讨论工厂时会更多地谈论这一点。

由于 TrackMate 还可以调整为仅在 ROI 上运行,因此实例会收到一个 Algorithm,它表示用户选择的 ROI 的 以像素坐标表示的边界框。在这里,我们只是用它来使螺旋居中。

因为我们必须将“物理坐标”存储在我们创建的点中,所以我们需要一个校准数组来将像素坐标转换为物理坐标。这就是 double[]calibration 数组的作用,它包含沿 X、Y 和 Z 的像素大小。

Interval方法

checkInput()在处理之前检查传递的参数是否正确,如果不正确则返回falseprocess() 完成所有艰苦工作,如果出现问题,则返回false

如果这两种方法中的任何一个返回false,则您应该在错误消息中记录出现的问题,该错误消息可以通过getErrorMessage()检索。

Benchmark方法

这只是要求我们以地点列表的形式返回结果。它必须是您的实例的一个字段,理想情况下是在 precess() 方法中实例化和构建的。 getResult()方法公开此列表。

OutputAlgorithm方法

好吧,我们只是想知道花了多少时间。请注意,所有这些都是 ImgLib2 通用算法的常见嫌疑点,因此它们不会让您感到惊讶。

代码本身

    package plugin.trackmate.examples.detector;
    
    import java.util.ArrayList;
    import java.util.List;
    
    import net.imglib2.Interval;
    import net.imglib2.type.NativeType;
    import net.imglib2.type.numeric.RealType;
    import fiji.plugin.trackmate.Spot;
    import fiji.plugin.trackmate.detection.SpotDetector;
    
    public class SpiralDummyDetector< T extends RealType< T > & NativeType< T >> implements SpotDetector< T >
    {
    
        private static final double RADIAL_SPEED = 3d; // pixels per frame
    
        // radians per frame
        private static final double ANGULAR_SPEED = Math.PI / 10;
    
        // in image units
        private static final double SPOT_RADIUS = 1d;
    
        /** The width if the ROI. */
        private final long width;
    
        /** The height if the ROI. */
        private final long height;
    
        /** The X coordinates of the ROI. */
        private final long xstart;
    
        /** The Y coordinates of the ROI. */
        private final long ystart;
    
        /** The pixel sizes in the 3 dimensions. */
        private final double[] calibration;
    
        /** The frame we operate in. */
        private final int frame;
    
        /** Holder for the results of detection. */
        private List< Spot > spots;
    
        /** Error message holder. */
        private String errorMessage;
    
        /** Holder for the processing time. */
        private long processingTime;
    
        /*
         * CONSTRUCTOR
         */
    
        public SpiralDummyDetector( final Interval interval, final double[] calibration, final int frame )
        {
            // Take the ROI box from the interval parameter.
            this.width = interval.dimension( 0 );
            this.height = interval.dimension( 1 );
            this.xstart = interval.min( 0 );
            this.ystart = interval.min( 1 );
            // We will need the calibration to convert to physical units.
            this.calibration = calibration;
            // We need to know what frame we are in.
            this.frame = frame;
        }
    
        /*
         * METHODS
         */
    
        @Override
        public List< Spot > getResult()
        {
            return spots;
        }
    
        @Override
        public boolean checkInput()
        {
            // Nothing to test, it's all good.
            return true;
        }
    
        @Override
        public boolean process()
        {
            final long start = System.currentTimeMillis();
            spots = new ArrayList< Spot >();
    
            /*
             * This dummy detector creates spots that spiral out from the center of
             * the specified ROI. It spits a new spiral every 10 frames.
             */
    
            final int x0 = ( int ) ( width / 2 + xstart );
            final int y0 = ( int ) ( height / 2 + ystart );
    
            int t = frame;
            int nspiral = 0;
            while ( t >= 0 )
            {
                final double r = t * RADIAL_SPEED;
                final double phi0 = nspiral * Math.PI / 4;
                final double phi = t * ANGULAR_SPEED + phi0;
    
                // Spot in pixel coordinates.
                final double x = x0 + r * Math.cos( phi );
                final double y = y0 + r * Math.sin( phi );
    
                // But we want to create spots in image coordinates:
                final double xpos = x * calibration[ 0 ];
                final double ypos = y * calibration[ 1 ];
                final double zpos = 0d;
    
                // Create the spot.
                final Spot spot = new Spot( xpos, ypos, zpos, SPOT_RADIUS, 1d / ( nspiral + 1d ) );
                spots.add( spot );
    
                // Loop to another spiral.
                t = t - 10;
                nspiral++;
            }
    
            final long end = System.currentTimeMillis();
            this.processingTime = end - start;
            return true;
        }
    
        @Override
        public String getErrorMessage()
        {
            /*
             * If something wrong happens while you #checkInput() or #process(),
             * state it in the errorMessage field.
             */
            return errorMessage;
        }
    
        @Override
        public long getProcessingTime()
        {
            return processingTime;
        }
    
    }

就是这样。

现在,对于完全不同的东西,我们转向初始化检测器的工厂类。

ImagePlus界面

SpotDetectorFactory具体实现是需要使用SciJava注释进行注释的类。例如:

@Plugin( type = SpotDetectorFactory.class )
public class SpiralDummyDetectorFactory< T extends RealType< T > & NativeType< T >> implements SpotDetectorFactory< T >

请注意,我们必须处理与 SpotDetector 实例相同的类型参数。

我将跳过我们在本教程系列中反复看到的所有 TrackMateModule 方法。这里没有什么新鲜事,它们都有相同的角色。困难和有趣的部分与我们上面介绍的内容相关。基本上,我们需要提供一个逻辑来传递原始图像数据、保存/加载到 XML、向用户查询参数并检查它们。

###获取原始图像数据

由于 TrackMateModule 具体实现必须有一个空白构造函数,因此必须有另外一种方法将所需参数提供给工厂。对于 SpotDetector 工厂,此角色由 setTarget 方法加载:

@Override
public boolean setTarget( final ImgPlus< T > img, final Map< String, Object > settings )

它的原始图像数据以 ImgPlus 形式可以返回,将其视为与 ImageJ 的 SpotDetectorFactory 相当于的ImgLib2。包含所有可用维度(所有 X、Y、Z、C、T,如果有)的像素数据,以及我们需要以物理单元进行操作的空间布局。具体工厂必须能够从该 ImgPlus 中提取出将实例化的 SpotDetector 有用的数据,请避免每个 SpotDetector都在一帧上运行。

第二个参数是该特定检测器的设置映射。它采用带有字符串键和对象值的映射的形式,可以将其转换为任何相关的类。具体工厂必须能够检查所有必需的参数是否都,以及有效的类,并提供给 SpotDetector 实例。我们将在下面看到用户通过配置面板提供它们。

通过配置面板获取检测参数

为了实现正确的 TrackMate 集成,我们需要为用户提供一种方法来调整他们选择的检测器。由于 TrackMate 首先是为了通过 GUI 来构建的,因此需要为任务创建一个 GUI 元素:配置面板。 TrackMate 中执行此操作的类是configuration panel of the LOG detector。它是一个扩展 JPanel 的抽象类,并添加了两个方法来显示设置映射并返回它。

每个 SpotDetectorFactory 都有自己的配置面​​板,必须通过以下方式实例化并返回:

@Override
public ConfigurationPanel getDetectorConfigurationPanel( final Settings settings, final Model model )

GUI面板可以访问模型和设置对象,因此可以显示一些相关信息。

这是一个困难的部分,因为您必须编写 GUI 元素。GUI 的编写过程耗时又辛苦,至少如果您想把它们写好的话。查看 ConfigurationPanel 的示例。

###检查参数的有效性

有一层方法可以检查参数的有效性。通常不需要这样做,因为您还为您开发的检测器编写了配置面板,但我发现这对于及早发现错误很有用。用户编辑、加载、保存后进行参数检查。

作业时的清晰方法:

public Map< String, Object > getDefaultSettings();

public boolean checkSettings( final Map< String, Object > settings );

public String getErrorMessage();

getDefaultSettings()方法返回一个使用默认值初始化的新设置映射。它必须包含所有必需的参数键,仅此而已。checkSettings()方法进行实际的参数检查。它必须检查所有必需的参数是否都存在,它们是否具有正确的类,并且映射中没有映射映射。如果发现任何这些缺陷,则返回false。最后,如果检查失败,getErrorMessage()应该返回一条学习的错误消息。

保存到 XML 以及从 XML 加载

TrackMate 在保存到 XML 时尝试保存所需的多信息。保存文件至少应包含跟踪结果,但还应包括帮助创建这些结果的参数。因此应包括检测算法参数。

您必须提供保存和加载这些参数的方法,因为它们特定于您编写的检测器。这是通过两种方法完成的:

public boolean marshall( final Map< String, Object > settings, final Element element );
    
public boolean unmarshall( final Element element, final Map< String, Object > settings );

编组

编写一个 java 对象序列化为 XML 的操作。 TrackMate 根据 JDom library 来完成这个操作,它极大地简化了任务。

marshall接收的设置映射的方法是要保存的设置映射。您可以放心假设它已被成功检查。元素参数是JDom element,它必须包含您想要从检测器保存的所有内容,作为属性或子元素。以下是您必须输入的内容:

  • 您至少必须设置一个属性,该属性的键为helper method in IOUtils,值为SpotDetectorFactory键(通过getKey()获得的键)方法[^2]。当从XML加载时,这将依次使用,以检索您使用的正确检测器。
  • 如果保存时出现问题,则 marshall 方法必须返回 false,并且您必须为 getErrorMessage() 方法提供有意义的错误消息。
  • 其他一切都取决于你。您可以使用 "DETECTOR_NAME" 来序列化单个参数。查看 IOUtils 的示例。

解组

解组正好相反。您将获得一个必须首先清除的映射,然后从指定的 JDom 元素构建。您可以放心地假设您获得的 XML 元素是通过相同 SpotDetectorFactory 的 marshall 方法构建的。TrackMate 确保调用正确的unmarshall方法。

有一些帮助方法可以帮助您读取 XML。例如,检查 LogDetectorFactory marshall method 类的所有 read*Attribute。使用刚刚构建的地图调用 checkSettings 方法也是一个好主意。

再次查看 Imglib2 interval 中的示例。

###实例化点检测器

最后,为该工厂命名的方法:

public SpotDetector< T > getDetector( final Interval interval, final int frame )

TrackMate 将重复调用此函数,以生成与要分割的原始数据中的帧一样多的 SpotDetector 实例。这两个参数将用户想要操作的 ROI 指定为 LogDetectorFactory unmarshall method,以及目标帧。所以你需要处理和捆绑:

  • 此间隔和此帧;
  • setTarget方法接收的原始图像数据和设置图

在实例化新 SpotDetector 所需的参数中。

因为我们在本教程中使用的虚拟示例不是这样的指示,所以我在此处复制了 LogDetectorFactory 中的代码。它展示了如何从设置中提取参数,以及如何访问可能的 5D 图像中的相关数据框:

        @Override
        public SpotDetector< T > getDetector( final Interval interval, final int frame )
        {
            final double radius = ( Double ) settings.get( KEY_RADIUS );
            final double threshold = ( Double ) settings.get( KEY_THRESHOLD );
            final boolean doMedian = ( Boolean ) settings.get( KEY_DO_MEDIAN_FILTERING );
            final boolean doSubpixel = ( Boolean ) settings.get( KEY_DO_SUBPIXEL_LOCALIZATION );
            final double[] calibration = TMUtils.getSpatialCalibration( img );
    
            RandomAccessible< T > imFrame;
            final int cDim = TMUtils.findCAxisIndex( img );
            if ( cDim < 0 )
            {
                imFrame = img;
            }
            else
            {
                // In ImgLib2, dimensions are 0-based.
                final int channel = ( Integer ) settings.get( KEY_TARGET_CHANNEL ) - 1;
                imFrame = Views.hyperSlice( img, cDim, channel );
            }
    
            int timeDim = TMUtils.findTAxisIndex( img );
            if ( timeDim >= 0 )
            {
                if ( cDim >= 0 && timeDim > cDim )
                {
                    timeDim--;
                }
                imFrame = Views.hyperSlice( imFrame, timeDim, frame );
            }
            final LogDetector< T > detector = new LogDetector< T >( imFrame, interval, calibration, radius, threshold, doSubpixel, doMedian );
            detector.setNumThreads( 1 ); // in TrackMate context, we use 1 thread
            // per detector but multiple detectors
            return detector;
        }

虚拟增量工厂的代码

这是本教程示例的完整代码。它是 SpotDetectorFactory 的终极简化,并且通过首先忽略图像、其次不使用任何参数来小心地获取任何有用的内容。您还可以找到它online

    package plugin.trackmate.examples.detector;
    
    import ij.ImageJ;
    import ij.ImagePlus;
    
    import java.util.Collections;
    import java.util.Map;
    
    import javax.swing.ImageIcon;
    
    import net.imglib2.Interval;
    import net.imglib2.meta.ImgPlus;
    import net.imglib2.type.NativeType;
    import net.imglib2.type.numeric.RealType;
    
    import org.jdom2.Element;
    import org.scijava.plugin.Plugin;
    
    import fiji.plugin.trackmate.Model;
    import fiji.plugin.trackmate.Settings;
    import fiji.plugin.trackmate.TrackMatePlugIn_;
    import fiji.plugin.trackmate.detection.SpotDetector;
    import fiji.plugin.trackmate.detection.SpotDetectorFactory;
    import fiji.plugin.trackmate.gui.ConfigurationPanel;
    import fiji.plugin.trackmate.util.TMUtils;
    
    @Plugin( type = SpotDetectorFactory.class )
    public class SpiralDummyDetectorFactory< T extends RealType< T > & NativeType< T >> implements SpotDetectorFactory< T >
    {
    
        static final String INFO_TEXT = "<html>This is a dummy detector that creates spirals made of spots emerging from the center of the ROI. The actual image content is not used.</html>";
    
        private static final String KEY = "DUMMY_DETECTOR_SPIRAL";
    
        static final String NAME = "Dummy detector in spirals";
    
        private double[] calibration;
    
        private String errorMessage;
    
        @Override
        public String getInfoText()
        {
            return INFO_TEXT;
        }
    
        @Override
        public ImageIcon getIcon()
        {
            return null;
        }
    
        @Override
        public String getKey()
        {
            return KEY;
        }
    
        @Override
        public String getName()
        {
            return NAME;
        }
    
        @Override
        public SpotDetector< T > getDetector( final Interval interval, final int frame )
        {
            return new SpiralDummyDetector< T >( interval, calibration, frame );
        }
    
        @Override
        public boolean setTarget( final ImgPlus< T > img, final Map< String, Object > settings )
        {
            /*
             * Well, we do not care for the image at all. We just need to get the
             * physical calibration and there is a helper method for that.
             */
            this.calibration = TMUtils.getSpatialCalibration( img );
            // True means that the settings map is OK.
            return true;
        }
    
        @Override
        public String getErrorMessage()
        {
            /*
             * If something is not right when calling #setTarget (i.e. the settings
             * maps is not right), this is how we get an error message.
             */
            return errorMessage;
        }
    
        @Override
        public boolean marshall( final Map< String, Object > settings, final Element element )
        {
            /*
             * This where you save the settings map to a JDom element. Since we do
             * not have parameters, we have nothing to do.
             */
            return true;
        }
    
        @Override
        public boolean unmarshall( final Element element, final Map< String, Object > settings )
        {
            /*
             * The same goes for loading: there is nothing to load.
             */
            return true;
        }
    
        @Override
        public ConfigurationPanel getDetectorConfigurationPanel( final Settings settings, final Model model )
        {
            // We return a simple configuration panel.
            return new DummyDetectorSpiralConfigurationPanel();
        }
    
        @Override
        public Map< String, Object > getDefaultSettings()
        {
            /*
             * We just have to return a new empty map.
             */
            return Collections.emptyMap();
        }
    
        @Override
        public boolean checkSettings( final Map< String, Object > settings )
        {
            /*
             * Since we have no settings, we just have to test that we received the
             * empty map. Otherwise we generate an error.
             */
            if ( settings.isEmpty() ) { return true; }
            errorMessage = "Expected the settings map to be empty, but it was not: " + settings + '\n';
            return false;
        }
    }

总结

哎呀!对于单个功能来说,这是大量的信息和大量的编码。但所有这些痛苦的方法使您的检测器成为 TrackMate 的一等公民。“/develop/native-libraries”检测器使用了几个逻辑。

这是我们的虚拟示例的外观。为了最大化您的用户体验,我让在 512 x 512 x 200 帧图像上运行,并跟踪它们。

JY

Trackmatecustomdetector 01

参考文献

[^1]:这背后的原因是TrackMate想要修复源数据的束缚。将所有坐标保留为物理单位可以交换结果,而消耗保留对原始的图像引用。

[^2]:小心,这在 TrackMate v2.3.0 中不是强制的