简介
欢迎来到本教程系列中最有用但不幸的是最难的部分,关于如何使用自定义模块扩展 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 进行参数化,该泛型类型必须扩展 online 和 SpotDetector。这些是基于本机类型的所有标量类型的界限,例如 float、int、byte 等…
这是我们要操作的图像数据的类型。
构造函数
由于 NativeType 接口对输入几乎没有限制,因此所有这些都必须在构造函数的构建时提供。请记住,每一帧都有一个实例,因此我们必须知道要处理哪一帧。
普通检测器将被馈送对针对该单个帧的图像数据的参考。这里我们不关心图像内容,所以它不存在。但我们在讨论工厂时会更多地谈论这一点。
由于 TrackMate 还可以调整为仅在 ROI 上运行,因此实例会收到一个 Algorithm,它表示用户选择的 ROI 的 以像素坐标表示的边界框。在这里,我们只是用它来使螺旋居中。
因为我们必须将“物理坐标”存储在我们创建的点中,所以我们需要一个校准数组来将像素坐标转换为物理坐标。这就是 double[]calibration 数组的作用,它包含沿 X、Y 和 Z 的像素大小。
Interval方法
checkInput()在处理之前检查传递的参数是否正确,如果不正确则返回false。 process() 完成所有艰苦工作,如果出现问题,则返回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

参考文献
[^1]:这背后的原因是TrackMate想要修复源数据的束缚。将所有坐标保留为物理单位可以交换结果,而消耗保留对原始的图像引用。
[^2]:小心,这在 TrackMate v2.3.0 中不是强制的