ImageJ-图像处理-全-
ImageJ 图像处理(全)
原文:
zh.annas-archive.org/md5/77fcea5d854e81569a81d3b0666cdf75译者:飞龙
前言
图像处理在科学和技术领域至关重要。然而,随着图像变得更大和更复杂,需要更加先进的处理技术。自动化也成为必要,这样你可以轻松完成简单任务,并专注于更复杂的问题。ImageJ 正是为了帮助您——作为图像处理开发中的关键强大工具之一,它让您能够从图像中提取更多有用的数据。
本书涵盖内容
第一章, ImageJ 入门,探讨了 ImageJ 的起源和使用,并讨论了如何在不同的平台上下载和安装它。我们还将查看 ImageJ 安装的基本文件夹结构,并配置它以供使用。
第二章, 使用 ImageJ 进行基本图像处理,讨论了 ImageJ 支持的图像类型。您还将学习如何从磁盘或 URL 加载图像。我们将查看 ImageJ 图像窗口的结构以及可以查看的信息。它还将处理图像缩放、校准、查找表、调整图像大小和调整通道。
第三章, 使用 ImageJ 进行高级图像处理,研究了不同类型图像的处理。我们将探讨可能导致图像损坏和降低其质量的噪声来源。您还将学习如何对图像应用不同的校正来修复这些问题。
第四章, 使用 ImageJ 进行图像分割和特征提取,探讨了将图像分割为前景和背景的方法。我们将考虑在灰度和彩色图像中设置阈值的不同方法。
第五章, 使用 ImageJ 进行基本测量,考虑了一些测量图像和时间序列内参数的方法。我们将应用前几章讨论的一些技术来从我们的图像中提取数据。您还将学习如何在单张图像中可视化动态数据(如 kymographs)。
第六章, 在 ImageJ 中开发宏,讨论了如何使用记录器创建宏以发现我们可以应用的命令和函数。接下来,我们将查看处理一个包含大量图像的文件夹并将结果图像保存到硬盘上的过程。最后,我们将查看批量处理模式,它允许 ImageJ 以类似的方式处理文件夹。
第七章,ImageJ 构造的说明,探讨了 ImageJ 中可用的宏和插件框架。我们将讨论 ImageJ API 公开的一些结构,用于脚本和插件。最后,我们将描述如何设置 IDE 以使用它作为独立项目或基于 Maven 的项目来开发 ImageJ 和插件。
第八章,ImageJ 插件的解剖,探讨了 ImageJ1.x 和 ImageJ2 的插件解剖结构。我们还将查看这两个框架中插件使用的某些特定结构。本章将探讨如何使用 IDE 或 ImageJ 提供的工具编译、运行和调试插件。
第九章,为分析创建 ImageJ 插件,使用 Maven 系统和 NetBeans IDE 从头开始开发一个插件。我们将讨论如何向我们的插件添加基本用户界面,使用户能够更改影响插件功能的一些参数。我们还将添加一个外部库,以提供 ImageJ 中未出现的新功能。
第十章,接下来去哪里,总结了前几章讨论的主题,并提供了可供您继续开发自己的插件的可用的进一步资源。本章还探讨了开发人员可用的某些更高级的技术。
您需要这本书什么
您需要以下软件来阅读这本书:
ImageJ 1.4x 或 Fiji
- NetBeans 8.0.2+
这本书是为谁而写的
这本书是为那些渴望使用该领域领先的图像处理和分析工具来处理图像处理的工程师、科学家和开发者所创作的。不需要对 ImageJ 有先前的了解。读者需要熟悉 Java 编程,以便使用 ImageJ 编写自己的程序。
惯例
在这本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。
文本中的代码词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称如下所示:“最重要的两个文件夹是macros和plugins文件夹。”
代码块设置如下:
varmyTools = newMenu("My awesome tools",
newArray("Macro_1", "Macro_2", "-", "Macro_3"));
macro"My awesome tools - C037T0b11MT7b09aTcb09t" {
cmd = getArgument();
if(cmd== "Macro_1")
runMacro("/PATH/TO/Macro_1_tool");
else if(cmd == "Macro_2)"
runMacro("/PATH/TO/some_other_tool");
}
新术语和重要词汇以粗体显示。屏幕上显示的词,例如在菜单或对话框中,文本中显示如下:“我们现在可以通过从菜单中选择分析 | 分析粒子…来执行粒子分析。”
注意
警告或重要提示将以这样的框显示。
小贴士
小技巧和窍门如下所示。
读者反馈
我们始终欢迎读者的反馈。告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正能从中获得最大收益的标题非常重要。
要向我们发送一般性反馈,只需发送一封电子邮件到 <feedback@packtpub.com>,并在邮件主题中提及书名。
如果您在某个领域有专业知识,并且您有兴趣撰写或为书籍做出贡献,请参阅我们的作者指南www.packtpub.com/authors。
客户支持
现在您已经是 Packt 图书的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。
下载示例代码
您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 图书的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便将文件直接通过电子邮件发送给您。
错误清单
尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然会发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进此书的后续版本。如果您发现任何错误清单,请通过访问 www.packtpub.com/submit-errata,选择您的书,点击错误清单提交表单链接,并输入您的错误清单详情。一旦您的错误清单得到验证,您的提交将被接受,错误清单将被上传到我们的网站,或添加到该标题的错误清单部分。任何现有的错误清单都可以通过从 www.packtpub.com/support 选择您的标题来查看。
侵权
互联网上版权材料的侵权是一个持续存在的问题,涉及所有媒体。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即提供位置地址或网站名称,以便我们可以追究补救措施。
请通过 <copyright@packtpub.com> 与我们联系,并提供疑似侵权材料的链接。
我们感谢您在保护我们的作者和我们为您提供有价值的内容方面的帮助。
询问
如果您在本书的任何方面遇到问题,可以通过 <questions@packtpub.com> 与我们联系,我们将尽力解决。
第一章. ImageJ 入门
欢迎来到 ImageJ 图像处理 第二版。ImageJ 是一个多功能的开源软件包,专为科学图像处理和分析设计。它用 Java 编程语言编写,允许实现统一的跨平台体验。它基于 1987 年在 Macintosh 平台上开发的 NIH Image 软件包,由 Wayne Rasband 开发。Rasband 仍然是 ImageJ 的活跃贡献者,他在 1997 年发布了第一个 ImageJ 分发版。它作为一个项目开发,旨在解决一个问题。2012 年,ImageJ 在《自然方法》杂志上庆祝了它的 25 周年。
ImageJ 分发版
目前,有基于或扩展原始 ImageJ 的不同分发版。基本的 ImageJ 软件包可在国家卫生研究院的 ImageJ 网站上找到(imagej.nih.gov/ij/download.html)。当前软件包的版本是 版本 1.50b,网站每月更新。这是 ImageJ 的核心分发版,包含主界面以及所有基本工具,用于加载、查看、处理和导出图像和数据。其他分发版包含这个核心包及其大多数功能,但您需要添加额外的功能和插件来为特定领域创建优化的界面。其中一些其他分发版仍然容易识别为 ImageJ,而其他则提供了完全不同的界面。
根据不同的科学领域,基于 ImageJ 的核心开发了不同的分发版。生命科学领域的主要分发版之一被称为 Fiji(Fiji Is Just ImageJ),可以在 Fiji 网站上找到(fiji.sc/Fiji)。Fiji 的基础是 ImageJ,但它附带了一大批预安装的功能(宏和插件),这些功能在生命科学领域的图像处理中常用。它专注于荧光显微镜,内置了分割、可视化和共定位的工具。它还包含图像配准、粒子追踪和超分辨率处理与重建的插件。它还拥有一个广泛的图像格式库,可以打开。这个库包括通过 Bio-Formats 插件提供的所有主要获取软件包的专有图像格式,如即将到来的章节所述。这个分发版的优点是它附带的大量插件以及非常用户友好的脚本编辑器。它还为 ImageJ 以及一些插件提供了广泛的更新机制。
对于天文学领域,开发了 ImageJ 的一个不同版本,命名为AstroImageJ(www.astro.louisville.edu/software/astroimagej/)。这个版本采用了 ImageJ 的核心实现,并补充了专门为天文学分析开发的特定插件和宏。它与 ImageJ 不直接兼容。为了这个版本,ImageJ 的核心进行了轻微修改。
从 ImageJ 衍生出来但具有不同用户界面的一个示例是Icy(icy.bioimageanalysis.org/)。Icy 分发版集成了 ImageJ,并且许多插件都是兼容的。然而,并非为 ImageJ 开发的每个插件都能在 Icy 中工作,反之亦然。在 Icy 分发版中,对细胞和点检测与跟踪有很强的重视。对插件开发也有很强的重视。为 Icy 平台开发的插件将按照设计实现文档和自动更新。用户还可以直接从界面中向开发者提供反馈,这是其他基于 ImageJ 的分发版所不具备的功能。一个可能的缺点是它需要安装几个外部库,最重要的是 VTK,这可能会在 Linux 系统上引起问题。
另一个不仅使用 ImageJ 处理数据,还帮助获取数据的分发版本被称为μManager,可以在www.micro-manager.org/找到。它作为插件从 ImageJ 内部加载,但提供了一个针对图像获取和硬件控制的独特界面。相机和显微镜驱动程序允许控制用于图像获取的支持硬件,然后可以直接输入到 ImageJ 中进行处理和分析。μManager 的一个使用示例是在 Open SPIM 项目中,它被用来控制一个 DIY 光片显微镜,获取图像并处理它们。
ImageJ 的用途
ImageJ 是一个处理图像和执行分析的优秀工具。它在许多科学同行评审出版物中使用,涵盖生命科学、天文学和物理学等多个领域的 1000 多篇论文。在生命科学中,它用于量化医学图像,以帮助检测病理标志物。它还用于处理和量化来自单细胞或单分子实验的数据,这些实验使用超分辨率技术,如STORM和PALM。在物理学和工程学中,它用于量化并可视化来自原子力显微镜的数据。在天文学中,ImageJ 用于分析来自望远镜和卫星的图像,并可视化来自观测站的数据。NASA 喷气推进实验室托管了一个中心节点,拥有丰富的数据集,可在pds.jpl.nasa.gov/下载。它包含有关行星任务以及其他研究领域(如大气或小行星)的信息。
由于它支持大量不同的图像格式,它是一个出色的图像查看器,并允许执行大量基于像素的操作。它还支持每个通道大于 8 位或 16 位的位深度图像。然而,它并不适用于除基于像素的操作之外的其他任何用途。如果您想使用基于矢量的操作,那么 ImageJ 不是您需要的工具(除非您想开发这项功能)。
除了常见的图像处理工具,如裁剪、旋转和缩放之外,它还支持多维度图像。可以处理和保存多达五维的图像。这些维度可以包括通道(多种颜色)、帧(时间点)和切片(Z平面),以及这些维度的任何组合。目前,不支持多点采集(在更大的XY空间中的不同位置)。还可以通过调整亮度、对比度或像素的颜色编码(查找表)来改变显示像素的强度。还有更高级的技术来纠正图像采集的伪影,如背景和漂白。
ImageJ 的默认图像格式是标记图像文件格式(TIFF)。这种格式允许存储多维数据,并支持许多用于校准、数据采集信息和描述的元信息字段。它还可以存储有关叠加等元素的信息。图形注释放置在图像的单独图层上。测量将受益于图像中包含的校准,从而允许以适当的单位快速反馈尺寸。然而,它不太适合不同类型的混合数据,如视频文件。使用FFMPEG插件可以打开和保存视频的图像数据,但不能打开音频轨道。编辑仅限于一小套转场和分层技术。对于需要图像和声音的视频编辑,可以使用非线性编辑器。它们允许有更大的控制。
它也可以用作图像转换工具。ImageJ 可以原生地读取许多图像格式,并且借助插件,许多专有格式也可以打开。一旦图像被打开,就可以将其保存为 ImageJ 支持的任何导出格式,包括但不限于 TIF、JPG 和 PNG 等图像格式,以及 AVI 和 MOV 等时间序列和Z 堆栈格式。它还可以用来改变其他软件导出图像的顺序和/或颜色。然而,它并不是作为一个通用的照片编辑器或非线性视频编辑器,因为它缺乏这些工作流程所需的某些专业工具。
ImageJ 的当前状态
自 2015 年初以来,ImageJ 已被超过 200 篇出版物引用,涉及从物理学和工程到医学和生物学的各个领域。许多出版物是关于为解决科学某个子领域中的问题而专门开发的插件。在 ImageJ 网站上,列出插件的页面有超过 1000 个插件可供选择。一些研究机构甚至有多个插件的集合,这些插件是在那里作为研究项目开发的。大多数,如果不是全部,都是开源插件,源代码完全可用。你可以调整和定制代码以满足你的需求。
ImageJ2
ImageJ 仍在积极开发中,并且定期向核心发行版添加新功能和错误修复。目前,正在开发一个改进的 ImageJ 系统。它被称为ImageJ2。ImageJ2 的目标是更好地支持多维数据,并创建一个更可扩展的平台,可以作为库而不是独立应用程序使用。它还将创建一个更一致的开发和扩展环境。正在开发的功能之一是 ImageJ 的更新机制。目前,可以使用中央存储库自动更新 ImageJ,ImageJ2 的一个目标是将此选项扩展到插件和其他功能,并允许跟踪错误和功能。然而,新 ImageJ2 系统的一个核心要求是向后兼容性。这个目标意味着现在开发的插件将在 ImageJ 的未来版本中保持功能。当前状态被标记为beta,这意味着插件是功能性的,但可能仍然包含错误,并且尚未针对性能进行优化。
SciFIO 和 OME-XML
与图像相关的其他发展是那些与图像格式和标准相关的。目前,所有主要商业采集平台都使用独特的专有文件格式存储图像数据。SCIFIO 项目旨在创建一个可扩展和集成的接口来处理不同格式的图像。它将支持更多图像格式,并允许在导入数据时设置附加选项,例如自动缩放、加载元数据和以不同的 ImageJ 图像类型加载数据。然而,它仍在积极开发中,并且一些功能在生产环境中(尚未)完全工作。
OME-XML(开放显微镜环境-XML)项目旨在创建一个包含所有图像和元数据的标准化格式的文件格式。这将促进显微镜图像数据的交换,无论使用何种设备进行采集。它主要关注生命科学领域的显微镜数据交换。它包含所有实验和设置数据以及像素数据,所有这些都在单个文件规范中。
生物格式
除了专注于跨多个采集平台集成采集和处理的 OME-XML 格式外,还在积极开发用于导入目前存在的许多图像格式的插件。这个插件被称为 Bio-Formats,主要关注生命科学领域的图像格式。然而,它也支持FITS数据,这在天文学和太空探索领域使用。目前,它支持(程度不同)140 种不同的图像格式,并将它们转换为 OME-XML 格式以在 ImageJ 中使用。
集成采集和处理环境
由于 ImageJ 是一个如此可扩展的应用程序,用于获取、处理和分析,因此不可能处理所有选项和扩展。在本版中,我将专注于图像处理和分析。我推荐 Fiji 发行版给 ImageJ 的初学者,因为它包含大量有用的功能,可以帮助您快速入门。另一个优点是 Fiji 附带了一个脚本编辑器,它具有许多功能,这些功能也是一些较大的 Java 开发套件所提供的。这些功能主要包括语法高亮和智能缩进。编辑器还包括一系列宏和插件模板,允许您从一个基本框架开始。
获取和安装 ImageJ
当前版本的 ImageJ 可以在支持 Java 的任何平台上运行。当您希望使用 ImageJ 或其他发行版时,可以下载适用于您特定操作系统的版本。ImageJ 的发行版可以与预包装的Java 运行时环境(JRE)一起下载。以下章节将解释如何在三个主要操作系统上获取和安装 ImageJ:Windows、OS X 和 Linux。
ImageJ 的安装
当您下载 ImageJ 的副本时,可以提供 JRE。如果您已经安装了 JRE 的副本,则可以下载不包含 JRE 的 ImageJ 以实现更快的下载。运行 ImageJ 的最小要求是 JRE 版本 1.6 或更高。对于某些发行版,特别是 Fiji,JRE 必须是 1.6 版本。这种限制是由于 Fiji 附带的上位器当前实现的问题,它无法更新 JRE。这个问题可能在将来得到解决。
由于 ImageJ 自带了其自己的 JRE,因此它可以提取到 USB 驱动器上,并从那里运行而无需安装。系统上的唯一限制取决于图像的大小。ImageJ 直接将图像加载到内存中,因此可用的系统内存需要足够大,以便存储您希望处理的图像。当内存需求超过 3 GB 时,需要 64 位操作系统和 64 位 JRE。
由于 ImageJ 是平台无关的,您可以在所有三个平台上使用相同的版本:Windows、OS X 和 Linux。唯一与平台相关的部分是 JRE;对于每个平台,都有一个特定的 JRE 安装。以下章节将解释如何在每个操作系统中安装 ImageJ。
在 Windows 上安装
为了在 Windows 上安装 ImageJ,您可以从 ImageJ 网站下载最新版本,网站地址为NIH (imagej.nih.gov/ij/download.html),或者从 Fiji 网站下载 (Fiji.sc/Downloads)。当从 NIH 网站下载时,有两个选择:适用于 32 位或 64 位系统的安装程序,以及当您希望在无安装权限的平台运行 ImageJ 时,可以选择 ZIP 存档。
当使用安装程序版本时,通常不建议您将 ImageJ 安装到程序文件文件夹中。当使用程序时,ImageJ 文件夹中的某些文件需要修改,因此当以普通用户身份运行时,可能会出现访问问题。此外,当安装或创建插件时,编译文件需要放置在 ImageJ 文件夹内的插件文件夹中。对于普通(非管理员)用户,此文件夹可能没有写入权限。或者,您可以专门更改 ImageJ 文件夹的访问权限。然而,从安全角度考虑,这并不推荐。
双击提取文件夹中的ImageJ.exe文件将启动 ImageJ。此文件是一个包装可执行文件,它调用ij.jar文件并使用提供的 JRE 来运行它。Fiji 发行版以 ZIP 存档的形式提供,可以在磁盘上提取并立即运行:

在 Mac OS X 上安装
ImageJ 以 ZIP 存档的形式提供,可以提取到应用程序文件夹内的一个文件夹中。Fiji 发行版可以下载为 DMG 文件,可以将其拖放到应用程序文件夹中。这将安装 ImageJ 文件夹,使其对所有注册用户可用。它还会在应用抽屉中创建一个 Fiji 图标。如果您想在 OS X 10.10(Yosemite)下导入或导出 QuickTime 电影,您需要从imagej.nih.gov/ij/download/qt/下载QTJava.zip和libQTJNative.jnilib文件到您家目录中的Library/Java/Extensions文件夹。
注意,在 OS X 10.7 及更高版本中,您第一次尝试运行 ImageJ 时可能会收到一个警告。这个警告会显示ImageJ 无法打开,因为它来自未知的开发者。这可以通过进入系统设置并在安全和隐私面板中点击允许按钮来解决。这应该可以防止将来再次出现此警告。或者,您可以从“允许从以下位置下载应用程序”部分选择“任何位置”选项。从安全角度考虑,不建议选择此选项,因为它也可能允许恶意软件执行。
注意,当 ImageJ(或 Fiji)在 OS X 上运行时,菜单栏不是主窗口的一部分,就像在 Windows 中那样:

在 Linux 上安装
ImageJ 可以通过从 NIH 网站解包分发到 Linux 平台。NIH 网站上的分发是 ZIP 文件格式,而 Fiji 分发是 tar.gz 文件格式。对于大多数 Linux 发行版,建议你将存档提取到你的家目录内的位置。这可以防止 ImageJ 文件夹上的写权限问题。文件夹中包含一个运行 ImageJ 的 shell 脚本。这个 shell 脚本被命名为 ImageJ。对于不同的桌面环境,存在创建到这个脚本的 shell 快捷方式的方法,以便可以从快捷方式运行它。
ImageJ 文件夹结构
安装 ImageJ 后,文件夹结构会放置在安装过程中指定的位置。这个文件夹的结构包含几个对 ImageJ 正常运行至关重要的关键文件夹。如果你在一个你没有写权限的文件夹中安装了 ImageJ,有两个重要的文件夹需要读写权限才能使 ImageJ 正常工作:plugins 和 macros 文件夹。此外,Windows 平台上的配置文件(pref.cfg)需要用户有写权限。如果没有写权限,设置将无法更改。下一节简要解释了 plugins 和 macros 文件夹的属性,以及 ImageJ 如何使用它们。
插件文件夹
最重要的两个文件夹是 macros 和 plugins 文件夹。当 ImageJ 加载时,会搜索这两个文件夹以查找可用的宏和插件。当你下载一个插件并将其放置在 plugins 文件夹中时,插件将在下一次 ImageJ 启动时被发现。下载插件时,有三种不同的文件选项可供下载:Java 源文件(.java)、编译文件(.class)或 Java 归档(.jar)。为了使插件在插件菜单中显示,.java 和 .class 文件名中需要至少包含一个下划线字符。对于 .jar 文件,归档需要包含一个 plugins.config 文件,该文件定义了菜单系统中的位置。这还有一个额外的好处,即 .jar 归档中的插件也可以在插件 menu 外部安装。下划线在插件菜单中会被替换为空格,或者如果最后一个字符是下划线,则完全删除。如果你将插件放在 plugins 文件夹内的一个文件夹中,这个文件夹名称也会出现在 插件 菜单中,但只有当它包含至少一个有效的插件时。如果你下载了一个插件的源文件,你可以通过从 插件 菜单中选择 编译和运行 并选择 Java 文件来创建一个可执行的插件。下一次 ImageJ 运行时,新的插件将被自动检测。
JAR 文件略有特殊。它们可以放置在plugins文件夹中,但不必出现在插件菜单中。JAR 文件包含一个清单,指定了其中插件的位置。此规范允许开发者将插件放置在插件菜单的特定子菜单中,而不管 JAR 文件放置在哪个文件夹。如果您创建了一组希望分组在插件菜单中的链接或相关插件,这将特别有用。
宏文件夹
宏文件夹包含 ImageJ 附带的一组宏,也是存储用户定义宏的默认位置。ImageJ 宏是具有.ijm扩展名的平面文本文件,尽管这个扩展名不是必需的。任何具有有效宏代码的平面文本文件都可以在 ImageJ 中运行。ImageJ 中的宏有自己的语言,类似于 Java,但有一些细微的差异。第五章,“使用 ImageJ 的基本测量”将探讨如何创建宏,并解释 ImageJ 宏中的语言结构。
配置新的 ImageJ 安装
安装 ImageJ 后,可以首次启动。在使用 ImageJ 时,需要做一些设置以允许成功处理。最重要的设置之一是 ImageJ 可用的线程数和内存。默认情况下,ImageJ 有 512 MB 的内存可用。这允许打开高达 512 MB 的图像,这对于大量用例来说是可以的。然而,随着当前图像获取的趋势,文件现在往往在 1 GB 或更多。因此,首先要做的是将 ImageJ 的内存设置为至少您认为需要处理的最大的图像大小。另一方面,Fiji 在安装时会自动分配可用系统内存的 50%,如果需要可以更改。
要设置分配给 ImageJ 的内存,您可以在编辑 | 选项 | 内存和线程…下找到内存设置。您可以设置最大内存为小于系统内存的任何值。请注意,在 32 位系统上,无法分配超过 3 GB 的内存。如果您希望分配超过 3 GB 的内存,则需要在一个 64 位操作系统上安装 64 位 ImageJ。
还可以使用命令行参数在启动 ImageJ 时传递内存大小。为此,使用以下命令从命令行运行 ImageJ:
javaw –Xmx1024m -cp ij.jar ij.ImageJ
这告诉计算机以 1024 MB (1024m) 的内存运行 ImageJ。如果您需要更多,则可以将1024m的值更改为任何合适的值。但是,请确保您不会使用超过系统可用的内存。
另一个需要验证的重要设置是在外观对话框中。对于图像处理,缩放图像时插值选项应该取消选中。当处理图像时,此选项可能会干扰或给出误导性的结果:

如果你打算处理包含大量白色像素的图像,建议你将选择颜色更改为默认黄色以外的其他值。这可以通过在首选项中的颜色设置中完成。在许多情况下,橙色或绿色是良好的默认值。
摘要
在本章中,我们探讨了 ImageJ 的起源和使用,讨论了如何在不同的平台上下载和安装它。我们查看了 ImageJ 安装的基本文件夹结构,并为其配置了使用。现在一切应该设置妥当,可以开始图像处理的初步步骤。
第二章. 使用 ImageJ 的基本图像处理
在遵循上一章的说明之后,你应该已经安装并运行了一个有效的 ImageJ。本章将涉及以下主题:
-
ImageJ 支持的图像类型
-
多维图像
-
加载和保存图像
-
查看和获取像素值
-
校准图像以进行测量
ImageJ 中的图像
ImageJ 广泛支持多种常见的图像格式,如 JPEG、PNG 和 TIFF。借助Bio-Formats插件,还可以加载广泛的专有图像格式(例如,STK 文件[Metamorph]和 LSM 文件[Zeiss]),以及某些医学图像格式(Dicom)和天文格式(FITS)。Fiji 的最新版本附带 Bio-Formats 插件,并支持几乎所有主要图像格式的文件。
本章将使用文件菜单中样本图像项中可用的某些图像文件。这些文件可通过互联网访问,因此需要有效的互联网连接。整个图像套件也可以从 ImageJ 网站作为一个单独的下载获取。当使用 Fiji 发行版时,可以通过转到文件 | 打开样本 | 缓存样本图像从菜单中本地缓存图像集。
我们将首先打开这些样本图像之一,以演示 ImageJ 中的一些图像功能。为此,按照上一章所述启动 ImageJ。转到文件 | 打开样本,并选择Boats (356K)。这张图片来自imagej.nih.gov/ij/images/boats.gif,展示了一些停泊在港口的船只。同样,也可以通过转到文件菜单中的导入 | URL来加载这张图片,复制之前的 URL 并将其粘贴到字段中。图片应该在新窗口中加载并显示如下:

标题栏显示了文件名(boats.gif)。标题栏下方是一个信息栏,显示了图像的关键参数:图像的大小(720x576 像素)、图像类型(8 位)和文件大小(405K)。接下来的部分将提供更多关于 ImageJ 中图像类型基础细节的说明。
图像类型
一张图像是由像素组成的,每个像素都有一个由位编码的值。位的数量决定了可以表示的灰度值或颜色的数量。接下来的部分将简要介绍 ImageJ 支持的不同的图像类型。
灰度图像
上一节中的船只图像是一个 8 位灰度图像,这意味着每个像素的值介于 0(黑色)和 255(白色)之间。灰度图像也可以是 16 位(值介于 0 和 65535 之间)和 32 位(浮点图像)。图像的灰度值用查找表(LUT)表示。对于 8 位图像,LUT 将 0 到 255 之间的值映射到计算机屏幕上红、绿、蓝三色的等量混合,以显示灰度级别。例如,中灰度级别 128 将在你的屏幕上显示为 RGB 值(128,128,128)。你也可以改变 LUT 的映射以不同的比例显示。通过改变 LUT,你可以改变屏幕上图像的颜色外观。如果你想给灰度图像一种绿色外观,你可以将 LUT 设置为绿色。这将告诉 ImageJ 将屏幕上的中灰度值映射到 RGB 值(0,128,0),从而看起来更暗绿色。对于 16 位和 32 位图像,虽然它们可以表示更多的灰度级别,但同样的原则适用。当创建宏和插件时,这些区别变得很重要,因为某些处理步骤只能在 8 位图像上执行。
彩色图像
彩色图像通常有两种位深度:8 位和24 位彩色。8 位彩色图像类型是索引图像,其中索引确定图像的颜色。8 位彩色图像的一个例子是 GIF 文件格式。它在其索引中存储多达 256 种颜色,这导致文件大小减小,但颜色数量减少。这些图像存储了一个 256 种红、绿、蓝(RGB)值的表(也称为调色板)。表中的每个条目都有一个索引,该索引在图像中引用,用于使用该特定颜色的像素。这种类型现在很少见,因为由于存储容量更大和互联网连接更快,较小的文件大小不再那么关键。通过从 ImageJ 菜单中选择图像 | 颜色 | 显示 LUT,你可以查看调色板或索引图像的索引列表。
RGB 图像,例如 JPG 或 PNG 文件,是包含 24 位信息的彩色图像:红色、绿色和蓝色通道各占 8 位。PNG 文件可以额外有 8 位用于透明度通道。除了 RGB 图像外,还可以使用不同的颜色空间生成图像,例如Lab和HSB。HSB 图像将 RGB 图像的三个分量分解为色调、饱和度和亮度通道。色调分量可以与像素的颜色(蓝色、绿色、紫色等)相比较。小的色调值用于红色和橙色,而中等值将代表青色和蓝色。高色调值代表品红色和红色。在这张图像中,你可以看到色调通道在 RGB 图像中的映射(S 和 B 通道保持白色):

注意,色调通道的映射是环形的。纯白和纯黑具有相同的颜色。它们从红色开始,以红色结束。
调整饱和度会使颜色更或更彩色。饱和度值较小会使颜色看起来更灰,而饱和度值较大会使颜色更纯净。以下示例显示了色调通道的水平渐变和饱和度通道的垂直渐变(亮度通道保持恒定;橙色框界定了不同的通道):

当您从下方的面板底部移动到顶部时,颜色会变得更淡,变得不那么鲜艳,在这种情况下,颜色变为白色。上方的颜色由亮度通道决定,在这个例子中是白色。
改变亮度值会使颜色更亮或更暗。高值保持颜色完整且明亮,而低值会使颜色看起来更接近黑色。以下图像显示了这种效果,其中水平渐变再次是色调通道,而垂直渐变是亮度通道(饱和度通道是均匀的白色):

实际图像将具有色调、饱和度和亮度的灰度值组合,这些组合在一起产生最终的颜色。
注意
图像类型之间的转换
将 16 位图像转换为 8 位图像会以丢失像素强度信息为代价。为此,请转到图像 | 类型并选择您希望转换到的图像类型。然而,并非所有转换都是可能的。例如,RGB 图像不能直接转换为 8 位灰度图像!
栈和超栈
上一节中描述的图像类型是大多数图形程序支持的基本图像类型。然而,ImageJ 支持一类不同的图像,这些图像由多个原始图像类型组合成一个单一对象:一个栈或超栈。额外的维度名称取决于它们所代表的信息。当通过在体积的多个级别上获取图像来在三维空间中获取图像时,得到的图像称为Z 栈。Z 栈中的每一张图像被称为一个切片。当成像不同颜色时,该栈被称为多通道栈,栈中的每一张图像被称为一个通道。最后,有一个栈包含随时间获取的图像,栈中的每一张图像被称为一个帧。超栈是包含超过三个维度图像的栈。例如,包含切片、通道和帧的栈将是一个 5 维超栈。以下几节将简要解释不同类型的栈和超栈。
彩色图像和多通道栈
多通道图像包含可以各自具有自己颜色的单个通道。一个 RGB 图像可以转换为具有红色、绿色和蓝色通道的多通道堆栈。在图像样本中选择荧光细胞(400K)可以找到一个多通道图像的示例:

窗口看起来与Boats图像相似。然而,在图像窗口的底部还有一个条形。这个条形在左侧有一个字母C,表示它有多个通道。每个通道都有自己的 LUT,在这种情况下,是红色(通道 1)、绿色(通道 2)和蓝色(通道 3)。标题栏下方现在也显示了一些额外的图像信息。它显示了当前通道(1/3)和总通道数,文本的颜色表示通道的颜色。
这允许每个通道都有 16 位信息,从而在三个通道中总共可以有 48 位信息。ImageJ 内部可以无问题地处理这些文件,但大多数其他程序无法处理这些图像。在保存这些图像时,您可能需要将它们转换为不同的位深度,以便在其他程序中使用。
ImageJ 允许您使用查找表更改多通道图像的颜色。默认为灰色,但其他选项包括红色、绿色、蓝色、青色、品红色和黄色。还有多色 LUTs,它们在一系列颜色中编码强度:

前面的图像展示了 ImageJ 中可用的各种查找表(LUTs)的几个示例:从左到右是绿色、红色、青色和光谱。
Z 堆栈图像和体积
当使用显微镜或 MRI 机器制作光学切片时,生成的堆栈将包含三个维度(X、Y和Z)的信息。Z 堆栈中的每一张图像被称为一个切片。多个切片组成一个体积,可以在 3D 中可视化。这将在下一章中讨论。图像的外观将与多通道图像相同。然而,在滑块旁边将不再是C,而是Z,以表明堆栈包含切片。
时间序列
当以固定的时间间隔拍摄图像时,生成的堆栈将是一个时间序列,包含每个时间点的图像,称为帧。滑块的外观将略有不同。旁边不再是字母,而是一个小型的播放按钮。当您点击它时,时间序列将以时间序列获取的速度播放(如果堆栈已校准)。
多维图像
除了多个通道、帧(对于时间序列)和切片(Z 堆栈)之外,还可以将这三个维度组合成一个单独的图像文件:一个 5D 图像。如果您打开 有丝分裂(26MB,5D 堆栈) 样本图像,图像窗口底部将出现两个额外的滑块:

顶部滑块再次用于通道(由 C 表示),中间滑块用于导航切片(由 Z 表示),最后一个用于导航时间帧(由播放按钮 ► 表示)。当您点击播放按钮时,时间序列将像电影一样开始播放,播放速度由帧间时间间隔决定。如果您想改变播放速度,您可以右键点击播放按钮。或者,您可以从菜单中选择 图像 | 堆栈 | 动画 | 动画选项… 并输入速度(以每秒帧数表示)。较大的值意味着更快的播放。
标题栏下方的栏,称为副标题,再次提供了额外的信息。它现在显示了所选通道(c:1/2)、切片(z:3/5)和帧(t:26/51)。这次,图片也是校准过的,副标题还显示了图片的尺寸(以微米表示,像素大小在括号中指示)。
提取图像和像素信息
如果您想了解更多关于图片的信息,您可以按 Ctrl + I (在 Mac 上为 ⌘+I)来获取一个包含图片信息的新的窗口。如果您对 5D 图片这样做,您将得到之前图片右侧显示的信息。这显示了所使用样本的简要描述以及图片的尺寸(以校准单位微米和像素表示)。它还告诉您每个通道的位深度(每像素位数:16)以及所使用的帧间时间间隔(帧间隔:0.14286 秒)。
注意
默认情况下,使用控制键,或在 Mac 上的 ⌘(命令)键作为快捷键是可选的。您可以通过转到 编辑 | 选项 | 杂项 来控制这一点。有一个标记为 要求控制/命令键 的复选框。当选中时,控制/命令键对于 ImageJ 中使用的 快捷键 是必需的。当一个快捷键需要 Shift 键时,无论设置如何,此键仍然是必需的!在这本书中,我将包括快捷键的控制/命令键。
要查看像素的值,您可以将鼠标光标放在感兴趣的像素上。在 ImageJ 主窗口的状态栏中,您可以查看以下信息:

有关于像素位置的信息(X 和 Y 坐标,如果图像已校准,则为校准单位)以及值(强度或灰度值)。ImageJ 中的坐标相对于左上角(原点)。如果加载了 Z 栈,也会提供当前的z坐标。在下一章中,我们将探讨可以从图像中获得的其他测量值。
注意
关于切片索引的注意事项
注意,像素的给定值假设第一个切片是 0,而标题栏以下的信息假设第一个切片是 1。当你开始开发自己的宏和插件时,这种区别可能变得很重要!
加载和保存图片
让我们以下面几节来查看加载图片和序列。
加载图片和序列
正如我们所见,我们可以通过从样本中选择或转到文件菜单中的导入 | URL来从 URL 加载图片。对于存储在磁盘上的本地文件,我们可以选择文件 | 打开…并浏览到包含我们的图片的文件夹。也可以将图像文件拖放到 ImageJ 主窗口中加载。如果你将整个文件夹拖放到 ImageJ 窗口中,该文件夹中的所有图片都将被加载。
如果你有一个包含单个图片的文件夹,你希望将其作为序列打开,你可以转到文件菜单中的导入 | 图像序列…。这允许你选择系列中的第一个图像文件,之后 ImageJ 将确定所有将被加载到单个图像窗口中的图像。可以使用正则表达式来限制导入的图像数量。
注意
导入图像序列
当导入一系列图片时,所有图片必须具有相同的类型(位深度)和相同的尺寸(宽度和高度)。如果任何文件具有不同的尺寸或位深度,导入将失败,ImageJ 将显示错误。如果同一文件夹中有其他应忽略导入的文件类型,请考虑将它们放在不同的文件夹中。或者,您可以在导入对话框的正则表达式字段中过滤它们。
图片加载完成后,窗口标题栏会显示文件名。当文件名特别长时,重命名窗口可能有益。可以通过从菜单中选择图像 | 重命名…或通过在图片上右键单击并从上下文菜单中选择重命名…来重命名窗口。
保存图片
ImageJ 允许你将图像保存为不同的文件类型。ImageJ 首选的文件类型是 TIFF,因为它允许存储额外的元信息、感兴趣区域的叠加和校准信息。ImageJ 支持多种图像格式。当你转到文件 | 另存为时,会显示一个图像格式列表。JPEG 和 PNG 格式是压缩格式。它们需要较少的磁盘空间进行存储。这意味着它们根据选择的压缩量需要更小的文件大小。TIFF 格式是无损格式,但它可以支持压缩。
保存图像时,重要的是要考虑保存的图像将用于什么目的。当你希望在以后量化图像或需要反复保存它时,不推荐使用有损压缩的文件格式,如 JPEG。每次将图像保存为 JPEG 文件时,都会损失一点质量。此外,JPEG 压缩针对的是平滑的颜色渐变,当应用于强度突然变化的荧光图像时,会产生伪影。以下截图显示了将图像打开并保存为 JPEG 文件 200 次后的示例。左侧是原始图像,右侧是保存的图像:

最明显的问题是眼睛周围,可以看到多个压缩伪影。在眼睛的白色部分,现在出现了原始图像中不存在的斑点。皮肤也显示出类似块状的模式。这是由于 JPEG 压缩基于 8 x 8 像素的块。在这种情况下,图像被放大到 200%,对于网页来说,这张图像可能仍然可以接受。然而,由于伪影,这张图像对于图像量化是不可接受的。当你需要多次保存图像,或者当你不确定需要进一步处理什么时,始终将图像保存为 TIFF 文件。如果你需要一个用于网页或演示的图像,你可以使用仅 8 位灰度或 RGB 图像,这些图像可以保存为 TIFF、JPG 或 PNG 格式。
图像校准
当你执行图像测量并且希望测量距离或面积时,你需要确保你的图像以适当的单位校准。对于 2D 图像或 3D 图像,你可以输入像素尺寸,对于时间序列,你可以输入帧之间的时间间隔。为此,你可以按Ctrl + Shift + P(在 Mac 上为⌘ + Shift + P)来显示属性对话框。这允许你设置测量单位(例如,微米μm)和宽度、高度和深度的值。这些值表示每像素的单位数。对于时间序列,帧间隔可以输入为秒。当勾选全局复选框时,此校准将应用于所有打开的图像。
在 ImageJ 中查看图像
为了更详细地检查图像,我们可能希望使用 ImageJ 中可用的查看图像的一些工具。要显示可用的工具,让我们使用我们之前打开的荧光细胞图像。你可能首先想做的事情是查看图像的细节。这可以通过转到图像 | 缩放 | 放大 [+]或按+键来实现。当你放大时,光标的位置确定缩放的中心。当前缩放级别在图像标题栏中指示,最大缩放级别为 3200%。当你放大时,窗口会重新缩放,直到它不再适合桌面。当你放大超过这一点时,窗口大小保持不变,当前缩放的位置在左上角以叠加形式指示:

大的蓝色方块代表整个图像,而内部的较小方块表示当前缩放显示的位置(在这种情况下,位于图像的左下角)。要缩小视图,从菜单中选择图像 | 缩放 | 缩小 [-],或者使用-键。
如果你注意到缩放后的图像显示了一些如图所示的伪影,请确保你已通过转到编辑 | 选项 | 外观…禁用了插值缩放图像,如第一章节所述:

有时候,当你获取图像时,曝光设置可能不是最佳状态。这意味着你没有使用可用的所有灰度值范围。为了仍然看到信号,你可以调整图像的亮度和对比度。为此,从菜单中选择图像 | 调整 | 亮度/对比度,或者按Ctrl + Shift + C。这种调整是非破坏性的。它不会改变图像文件中的值,直到你在亮度/对比度对话框中按下应用按钮(对于 8 位和 16 位图像)。对于 32 位图像,应用按钮不起作用。如果你按下重置,值将恢复到初始值或按下应用后的值。如果你有一个曝光不足(暗)的图像,你可以通过降低最大值滑块或增加亮度滑块来使其变亮。
注意
亮度/对比度调整和测量
当你调整图像的亮度/对比度并应用它时,图像的灰度值将不可逆地修改。如果你仍然希望执行包括强度值的测量,应用这些修改将改变你的结果(也许,结论)。只有在创建用于非关键查看(演示)或测量长度或面积而不依赖于强度时才使用应用按钮。
查看多通道图像
当您有一个多通道图像时,您有时可能想隐藏某些通道以避免查看。ImageJ 允许您通过在菜单中选择图像 | 颜色 | 通道工具…或按Ctrl + Shift + Z来显示或隐藏多通道图像中的任何通道。每个通道都有一个复选框的对话框打开。当复选框被勾选时,该通道被显示。否则,它会被关闭(隐藏)。您还可以通过转到图像 | 颜色 | 排列通道…来更改通道的顺序。当调整多通道图像的亮度/对比度时,调整仅应用于当前显示的通道。当前选定的通道可以在标题栏直接下方的信息栏中检查。亮度/对比度对话框中直方图的颜色也反映了所选通道的颜色。
注意
亮度/对比度调整和通道工具
当您使用通道工具隐藏通道时,它们仍然可以通过亮度/对比度对话框进行修改。如果您在修改亮度/对比度时当前选定了隐藏通道,ImageJ 会进行调整,但这些调整是不可见的。在调整之前,始终要验证当前通道!
查看时间序列
当查看时间序列时,亮度和对对比度调整在时间序列的所有帧中都是可见的。如果您想应用调整,ImageJ 会询问您是想对当前帧还是对所有帧进行操作。当您将设置应用于所有帧时,调整对所有帧都是相同的,无论帧中的强度如何。这意味着这并不适用于显示荧光随时间减弱的时间序列(即漂白)。漂白是荧光成像的固有特性,会导致强度随时间降低。一般来说,这种效应遵循指数衰减趋势。Fiji 提供了一个选项,通过选择图像 | 调整 | 漂白校正从菜单中,来纠正这个漂白过程。对于大多数经历漂白的时间序列,选择指数拟合作为最佳校正方法。这种方法对来自非漂白源的强度变化更具鲁棒性。如果强度变化的原因不同,您可能想使用简单比率方法来校正时间序列。运行此校正会在新的图像窗口中显示校正后的数据,这意味着原始数据保持不变。
摘要
在本章中,我们讨论了 ImageJ 支持的不同图像类型。您还看到了如何从磁盘或从 URL 加载图像。我们研究了 ImageJ 中图像窗口的解剖结构和可以查看的信息。我们对图像进行了校准,以便进行长度和面积测量。最后,我们探讨了查看不同图像类型的不同方法。您学习了如何调整图像的亮度和对比度。
在下一章中,我们将探讨使用 ImageJ 界面执行基本处理步骤的方法。
第三章. 使用 ImageJ 的高级图像处理
上一章向您展示了如何在 ImageJ 中加载和查看图像,以及如何对图像强度和像素值进行基本修改。本章将讨论预处理图像所使用的技巧。我们将为图像分析和测量做准备。本章将应用我们在前几章中探讨的一些技巧。我们将涵盖以下主题:
-
纠正图像
-
Z 栈处理
-
时间序列处理
-
图像和栈计算
纠正图像
为了分析图像,我们有时需要纠正采集过程中出现的问题。例如噪声、不均匀的照明和背景荧光等问题,在图像分析过程中可能会引起许多问题。我将简要介绍这些问题的来源,然后说明如何在 ImageJ 中纠正这些问题。
技术背景
在成像中存在的许多噪声来源中,一些可以通过正确的采集设置来纠正。其他噪声则存在于相机的电子和物理特性中,并且难以修复。我将首先处理可以通过优化采集来修复的噪声来源:随机或泊松噪声。接下来,我们将探讨电子或暗噪声。
纠正随机噪声
随机噪声是由光的物理特性引起的;光可以被看作是光束或光子。每个相机光电探测器的像素收集到的光子数量决定了最终的像素强度。如果在任何时刻只有少数光子击中探测器,光子数量的差异可能会很大。这被称为泊松过程,信噪比可以表示如下:

这意味着随着光子数量(N)的增加,信噪比(SNR)将增大。通过增加曝光时间或照明强度,每个像素的光子数量和 SNR 将增加。低信噪比不能通过软件处理技术来修复。
纠正暗噪声
另一种噪声来源被称为暗噪声或暗电流。这种噪声来源来自相机中的电子元件,可以通过在不照明的情况下拍摄图像来可视化。在数字消费相机中,当镜头完全覆盖时曝光图像可以轻松实现这一点。您甚至可以用手机的相机尝试。只需紧紧覆盖镜头并拍照(确保闪光灯已关闭!)!例如,以下图显示了由两个不同相机拍摄的一小部分图像,它们的设置相同。左侧图像是使用索尼α6000(2014)拍摄的小区域图像,而右侧图像来自佳能 EOS 550D(2010)。橙色条用来区分它们:

为了显示图案,每个图像的强度都被均衡了,在这个例子中只显示了绿色通道。两个相机的设置如下:1/10 秒曝光,ƒ5.6,ISO 200。从这些图像中可以看出,两个相机传感器之间的电子噪声水平相当不同。请注意,大多数科学相机,尤其是冷却的EM-CCD(电子倍增电荷耦合器件)相机,电子噪声水平要低得多。这使得一些 EM-CCD 相机能够检测单个光子,甚至计数它们。
为了使暗噪声信号的减法工作,曝光时间需要与获取时的曝光时间相同,以获得相同的暗噪声水平。曝光时间直接与噪声量相关。较长的曝光时间会导致更多的暗噪声。这种噪声可以通过稍后在本章中介绍的图像计算器在 ImageJ 中轻松修复。
为了确定你自己的相机的噪声水平,用镜头盖住相机拍照(确保它完全被所有光线遮挡)。理想情况下,你应该使用你的相机以 RAW 文件格式捕获图像。当相机以 JPEG 文件格式获取图像时,相机已经对图像进行了某些噪声降低处理。如果你只能以 JPEG 格式捕获图像,请检查是否有选项可以关闭噪声降低。现在,打开图像在 ImageJ 中,如前一章所述,并按照以下步骤操作:
-
通过点击它来选择你的暗光图像窗口,使其处于活动状态。在 ImageJ 中,大多数命令将作用于活动图像或最后打开的图像。通过点击一个图像窗口,该图像就变成了活动图像。
-
为了确定噪声水平,我们可以选择一个我们想要测量的区域。通过输入特定的值来指定它,我们将创建一个矩形。为此,转到编辑 | 选择 | 指定…,然后选中居中复选框,输入
512作为宽度和高度。对于X 坐标和Y 坐标,输入图像宽度的一半和高度的一半(在图像副标题中指示)并点击确定。 -
确保测量设置为标准差。这可以通过转到分析 | 设置测量并选中标准差复选框来完成。选择其他测量参数是可以的,在下面的输出中面积、平均灰度值和最小和最大灰度值也被选中。对于这个练习,标准差选项是唯一需要的相关参数。
-
通过按Ctrl + M或转到分析 | 测量来执行测量。你可以在放置区域后立即测量它们,或者你可以在测量之前将它们添加到ROI 管理器(下一章将详细介绍)中。
现在的结果应该在新窗口结果中可见。根据你选择的参数,此窗口中的结果可能与这里显示的不同(我包括了区域和最小值和最大值):

第一行包含α6000 相机的结果,第二行包含 EOS 550D 相机的结果。两个测量的区域相同(512 x 512 = 262144 像素),但第一台相机的标准偏差(噪声的度量)比第二台相机低 6.3 倍。此外,第一台相机的平均值更接近 0,正如你所预期的,当没有光线击中传感器时,值应该是这样的。
注意
相机可能会有不再工作的像素(死像素)。死像素在明亮区域会显示为黑色像素,并且总是出现在相同的位置。相反的情况也可能发生。在暗区域非常明亮的像素被称为热像素。热像素不必每次都出现在相同的位置,并且在使用非常长的曝光时间时更为常见。对于 EMCCD 相机,还有一个导致明亮像素的来源,这是由宇宙射线撞击图像传感器造成的。这些事件在长时间序列中相对常见,表现为仅在一帧中非常明亮的区域。去除死像素和热像素的方法与暗噪声去除相同。
对于大多数曝光类型,这些噪声级别非常小,不会导致你的照片质量下降。海滩上的照片,天空中有太阳,不需要校正。由于检测到的光量巨大,电子噪声被完全淹没。然而,在图像采集领域,暗噪声是一个重要因素的是天体摄影或夜间摄影。每当需要长时间曝光进行图像采集时,电子噪声就会成为一个重要因素,可能会降低你的图像质量。
为了减少低光条件下传感器噪声的影响,你需要稍微改变你获取图像的方式:不是单次曝光,而是需要连续多次曝光。一些相机支持这种自动模式,例如手持黄昏模式(索尼)或多帧降噪模式(宾得、奥林巴斯等)。在此模式下,你将快速连续拍摄 2 张或多张照片,最终图像是这些图像的平均值。你还可以在 ImageJ 中使用以下步骤进行类似操作:
-
打开你连续获取的多张图像(确保没有其他图像打开!)
-
从菜单中选择图像 | 堆栈 | 图像堆栈。现在你将有一个单独的窗口,其中每个切片代表你拍摄的一张图像。
-
通过从菜单中选择图像 | 堆栈 | Z 投影…来创建降噪图像,并使用平均强度作为投影类型。
需要记住的是:当任何东西在个别曝光之间移动时,这种方法将不会提供良好的结果。它可以纠正简单的位移,但这仅在最简单的情况下有效。
不均匀照明 – 背景减法
当在困难的光照条件下获取图像时,有时可能会出现图像传感器上的照明不均匀。这种不均匀照明的效果是可以在 ImageJ 中轻松纠正的。为了展示如何进行这一操作,我们将使用带有微分干涉对比度(DIC)光学系统的倒置显微镜获取的明场照明图像。
注意
DIC 图像通过观察样品厚度的差异来提供对比度。一束光波被分成两束稍微分离但平行且具有相同相位的单独光束。当一束光通过比平行光束密度更高的物体时,波将移出相位。当它们重新组合时,出相位的射线将部分相互抵消(干涉)。这导致相机像素上的光减少,使像素变暗。对于细胞,最强的干涉可以在细胞膜附近找到。一束光将穿过细胞,而平行光束将穿过细胞外的水。
该图像显示了不均匀照明的影响。画框的左侧比中间暗,梯度沿着画框以略微斜向的方向运行。很明显,场域不是向一个方向移动。中间是最亮的,而两个边缘,左侧和右侧,则较暗:

作为第一次尝试,我们将使用背景减法方法来查看这能否解决问题。为此,我们需要进入处理 | 减去背景…并使用以下设置:

应用背景减法后,图像发生了变化,但背景不均匀的效果仍未得到纠正。实际上,图像的左下角稍微暗一些,而且中间的强度也没有减少很多(参见左侧图像)。请注意,当选择了光背景选项(参见右侧图像)时,左右两侧都有强烈的过度补偿。不仅这些侧面的对比度降低了,而且照明现在比校正前更加不均匀:

禁用滑动抛物面选项也导致了更加不自然和错误的伪影。这种类型背景减法的问题在于它假设背景有一个均匀递减的变化。这意味着背景的变化应该是平滑的,并且从高到低在一个方向上(例如从左到右、对角线等)进行。然而,像这样的 DIC 图像往往有一个更类似于 U 形的背景:边缘高而中间低,或者相反。因此,这种方法不适用于这种类型的图像,需要探索其他方法来解决这个问题。
接下来,我们将尝试使用伪平面场校正法来消除背景。这种方法基于使用高斯滤波器对图像进行过滤,该滤波器会模糊细节。这个过滤器将捕捉到不均匀的照明并将其从帧中的物体中分离出来。这些过滤器如何工作的基础将在下一章中更详细地讨论。让我们创建一个背景图像,我们将用它来校正不均匀的照明。你需要执行以下步骤:
-
首先,我们想要复制图像,以便保留原始图像用于减法。为此,我们将转到图像 | 复制…或使用Ctrl + Shift + D,并将复制的图像命名为
background。 -
要创建高斯低通滤波器,我们将选择背景图像并转到处理 | 滤镜 | 高斯模糊…,为 sigma(半径)输入
150的值。当你勾选预览复选框时,你会看到图像看起来像是失焦的。你可以看到物体已经无法区分,剩下的只是对角线背景照明。 -
现在,我们可以从原始图像中减去这个背景来校正不均匀的照明。为此,我们将通过从菜单中选择处理 | 图像计算器…来启动图像计算器。然后,我们将原始图像选为Image1,背景图像选为Image2。将操作设置为减去,并勾选创建新窗口和32 位(浮点)结果复选框。以下图像显示了减法的效果以及它是如何校正不均匀照明的:
![不均匀照明 – 背景减法]()
图像归一化
要增强曝光不足的图像的对比度,你可以转到处理 | 增强对比度…选项,并选择归一化复选框。这将在整个 8 位或 16 位图像的范围内拉伸灰度值。它不适用于 RGB 图像。以下图像显示了归一化的效果,原始图像在左侧,归一化图像在右侧:

这也适用于堆叠或时间序列,其中可以对每个帧分别进行归一化。使用亮度/对比度窗口中的自动选项可以获得类似的效果,如前一章所述。请注意,归一化应用于图像并不可逆地修改像素值。如果信号不应随时间变化,这不会对测量造成大问题。然而,对于随时间变化的强度变化,这种方法将扭曲或去除这些变化。
漂白校正
在成像荧光时,照明可能会引起研究中的荧光素的漂白。这种效应已经得到很好的建立,并且与激发光的强度有关。为了避免这种效应,最好使用长时间曝光和低强度光。然而,这并不总是可能的。漂白的量与初始强度有关,并以指数方式减少。为了判断图像序列是否受到漂白的影响,我们可以对每个帧的整个图像进行快速测量,以查看平均强度。请注意,如果单个帧中存在照明或背景信号的变化,结果可能看起来不是一个平滑的曲线。为了快速测量,请按Ctrl + A选择整个帧,然后按Ctrl + M测量强度。对每个帧重复测量,并在您喜欢的图形程序中将平均强度值与帧号(或如果您知道间隔,则为时间)绘制成图。在这种情况下,我使用了MATLAB来创建图表,尽管您也可以通过从 ImageJ 的菜单中选择图像 | 堆叠 | 绘制 Z 轴轮廓来创建图表。以下是一个漂白曲线的示例:

这组数据点似乎遵循一个接近直线或指数曲线的趋势,尽管前 2 秒的趋势似乎更接近指数而非线性。
为了执行漂白校正,您可以通过在 Fiji 中选择图像 | 调整 | 漂白校正来选择校正插件。漂白校正有三种方法:
-
简单比例
-
指数拟合
-
直方图匹配
如果强度的减少不遵循规则形状,例如指数衰减函数,那么简单比例是最好的方法。对于大多数荧光成像,这种方法可以得到良好的结果,并且可以与荧光测量相结合。直方图匹配方法在噪声图像上表现更好,但不太适合强度测量。
由于我们的趋势看起来更像是指数衰减模型,我们选择了第二种方法,即对数据进行单指数函数拟合:

此图是使用漂白校正命令获得的参数生成的,并将它们输入 MATLAB 中。ImageJ 命令本身也会生成带有数据和拟合曲线的图表。然而,轴被标记为X和Y。因此,为了清晰起见,我已重新创建了带有正确标签的图表。红色线表示拟合函数,它与曲线匹配,R²值为.9954(一个非常好的拟合)。该模型由三个参数组成,标记为a、b和c。a的值表示第一个点位于由c值指示的渐近线之上的程度。渐近线是这个指数曲线在给定无限时间时将趋近的值。b的值表示曲线衰减的速度。如果您想知道失去初始荧光一半所需的时间,您可以使用以下公式,使用拟合中的b值:

前面的公式给出了荧光衰减的半衰期。请注意,拟合的b参数是以帧为单位表示的,而不是时间。因此,在使用前面的公式时,您需要将结果乘以您的帧间隔以获得以秒(或分钟)为单位的价值。在前面显示的图表中,半衰期为 30.587 秒(使用b值为 0.0028327 和帧间隔为 0.125 秒的公式)。
堆栈处理
ImageJ 非常适合处理具有超过两个维度的信息:在不同 Z 层或不同时间点获取的数据。我们已经在噪声校正部分的示例中看到了堆栈处理的例子。下一节将处理由帧组成的时序。然而,首先,我们将探索处理包含切片(Z 堆栈)的图像堆栈时更多的选项。
处理 Z 堆栈
Z 堆栈是在不同高度或距离处获取的一系列二维图像。在显微镜中,这是通过上下移动物镜或载物台并在特定间隔获取图像来实现的。在磁共振成像(MRI)中,这是通过将患者通过扫描仪中心移动来实现的。扫描仪然后使用产生磁场波动的无线电脉冲为每个位置创建图像。这些波动可以通过 MRI 机器中的探测器来测量。这导致一个可以组合成单个文件的单一切片。您可能想要对这类图像执行的一些处理包括创建体积的投影或 3D 渲染。我们首先将检查您可以创建的投影。然后,您将理解为什么您会创建它们。
堆栈投影
我们已经在噪声消除部分的章节中看到了 Z 投影的一个例子。在前一节中,我们使用投影为每个像素的帧创建平均强度。对于包含切片(Z 信息)的图像,平均投影通常不是最有用的投影。然而,ImageJ 中还有其他适用于 Z 堆栈的 Z 投影。以下几节将处理这些投影的一些示例。
最大投影
最大投影使用每个像素在切片中的最大强度。如果一个堆栈有 20 个切片,那么每个像素将包含这 20 个切片中的最大值。这种类型的投影有助于减少 Z 堆栈的第三维度,以便创建数据的二维表示。这种投影本质上会平坦化图像。当用于具有稀疏信号(在同一位置少量明亮像素)的荧光图像时,这种投影的效果是显示单个帧中的所有物体。它也适用于具有薄物体且在不同切片的不同位置聚焦的荧光图像。通过平坦化 Z 堆栈,所有聚焦的部分将在一个连续的形状中可见。你可以将此想象为一组楼梯。每个台阶有不同的 Z 位置,但如果你将台阶平坦化(假设台阶不重叠),你会得到一块矩形板。如果你有一个非稀疏的图像,那么这种投影就几乎没有用处。为了演示这种投影,请从样本图像中打开共聚焦系列图像。转到图像 | 堆栈 | Z 投影…并选择最大强度作为投影类型。以下图像显示了这种投影的结果:

如前图所示,最大强度投影显示了整个细胞的形状,但一些信息丢失了。具体来说,第一帧中的小细节被体积中间的强烈像素淹没。对于某些表示,这是可以的。对于某些 Z 堆栈,单个切片中总体积的粗略形状不清楚,但最大投影显示了总体形状。
为了演示这种效果,我们将通过转到文件 | 打开样本打开蝙蝠耳蜗体积图像。查看这个体积的几个切片几乎无法提供有关这个感觉器官(数字表示切片编号)的形状信息:

当我们创建最大强度投影时,这个器官的一般形状变得更加明显:它呈扭曲螺旋状(耳蜗是用于听力的壳形腔室)。当你通过转到文件 | 打开样本打开蝙蝠耳蜗渲染图像时,你会看到体积的 3D 渲染:

左侧的图像显示了 Z 堆栈的最大投影,而右侧的图像显示了渲染的体积。从这个图像中可以看出,最大投影比单个切片提供了更多信息。然而,在处理过程中,一些细节仍然丢失。特别是耳蜗的左上部分在最大投影中非常不清楚。螺旋的开始被它后面的环所遮挡。
Fiji 有一个选项允许你创建包含更多信息的最投影:深度编码。深度编码将颜色分配给像素的 Z 位置,导致不同切片有不同的颜色。要在 Fiji 中这样做,转到图像 | 超堆栈 | 时间-颜色编码,并选择灰度作为 LUT。这会导致前一张图像中的中间表示(不是完全一样,但非常相似)。请注意,你会收到一个消息,切片和帧已被交换。这是由于这个插件是为时间序列设计的,而不是 Z 堆栈。前一张图中的右图是体积的 3D 渲染,将在下一节中介绍。
体积查看和渲染
当在深度范围内获取图像时,通常的目标是将这些图像集合作为 3D 体积查看。另一个有用的查看方面是取一个三维体积,沿着 z 轴切割,并从侧面查看体积。这种后视图不能从二维图像中获得。对于这个例子,我们将使用样本图像的两个不同的堆栈。让我们从查看 MRI 堆栈的体积开始。要打开图像,转到文件 | 打开样本并选择MRI 堆栈。这是一个 MRI 堆栈,其中每个切片都位于头部不同水平(数字表示切片号):

在切片 6 中,眼睛清晰可见,图像顶部有两个暗色的球体。切片 11 显示了头骨内的脑部,而帧 16 显示了头部中间的黑洞,即脑室。切片 26 显示了头顶。该区域较小,表明头部顶部正在接近。
切片中已经包含了很多信息。眼睛、鼻窦和鼻子(切片 1)都可以清晰地看到。然而,这些切片中的信息并不完整。我们在 z 轴方向上缺少形状。要查看这个体积的三维形状,我们可以使用 Fiji 附带的三维查看器。选择 MRI 堆栈后,转到插件 | 体积查看器。如果你有标准的 ImageJ,可以从插件页面下载并安装体积查看器插件。然后,将打开以下窗口:

这是体积查看器,它是 Fiji 的一部分,并且作为 ImageJ 的插件可用。在左侧,有三个图像显示了体积的不同视图:一个xy切片(这是这个堆栈的顶部视图),一个yz切片(这是这个堆栈的侧面视图),最后是一个xz切片(这是这个堆栈的前视图)。查看器中间的大图像是当前选定的视图,在这种情况下,是xz切片视图。这个切片的位置在左侧的概述图像中由青色(xy)和绿色(yz)线条指示。请注意,我通过输入5并按Enter键而不是基于当前校准的值来调整z-Aspect。否则,体积看起来非常扁平。这种扁平的外观是由于这幅图像没有校准。每个体素(体积和像素的缩写)是 1 x 1 x 1,没有单位。MRI 图像中体素大小的典型值是 1.5 x 1.5 x 3.0 mm,这可以通过上一章中描述的图像属性来设置。现在我们可以通过选择查看器底部的视图按钮来更改视图。yz按钮将给我们这个体积的侧面视图。也可以通过点击并拖动鼠标来旋转体积。
注意
体积查看器窗口的大小至少为±1024 x 768 像素。这意味着根据您监视器的像素尺寸,一些控件可能超出屏幕。对于大多数现代显示器,这应该不会成为问题。然而,对于一些小屏幕或投影仪,这可能会成为一个问题。
接下来,我们将查看不同类型的图像:使用荧光成像的果蝇大脑的 Z 堆栈。要打开图像,转到文件 | 打开样本并选择果蝇大脑图像。Z 堆栈将打开,你可以浏览切片:

第一层切片中没有明亮的像素,但随着你在堆栈中移动,果蝇的大脑开始显示出定义明确的特点。这个堆栈显示了大脑的(旋转)前视图,与之前显示为顶视图的 MRI 堆栈形成对比。我们将使用体积查看器来检查整个体积,并用它来创建一个体积旋转的短片。首先,选择果蝇大脑堆栈,然后转到插件 | 体积查看器。初始图像将是一个切片视图,但在这个例子中,我们想要切换到不同的模式。使用查看器顶部的选择器选择体积(4)模式。我们将使用下拉选择器将插值设置为三阶锐化(3)。在查看器的右侧,我们将修改传输函数为2D 渐变以创建一个稍微更令人愉悦的视图。接下来,我们将体积查看器底部的旋转设置为X、Y和Z分别为-90、30 和 180。这将提供一个大脑的侧面视图。
通过在查看器中按下快照按钮(右上角),我们将获得当前视图的图片。接下来,我们将以 10 度的增量增加Y旋转的值,并在每次达到 210 度时拍摄快照。我们现在从大脑的一侧到另一侧(180 度)拍摄了大脑的快照。要将这些转换为动画,我们只需转到图像 | 堆栈 | 图像到堆栈。如果您关闭原始堆栈,您只需在对话框中按确定即可。否则,您必须在标题包含字段中输入Volume_Viewer。现在您将有一个可以播放并保存为电影以供演示目的的堆栈。在这个例子中,我们使用了 10 度的增量进行旋转,这给出了足够的结果。然而,如果您使用更小的增量,结果看起来会更平滑。您可以自由地修改观察体积的角度以获得不同的结果,也可以在体积查看器中实验其他设置。
体积查看器是 ImageJ 中的一个非常强大的功能,它允许调查和可视化 3D 对象。使用切片(0)模式来检查体积作为横截面,使用体积(4)模式来查看实体模型。
处理时间序列
时间序列由随时间获取的图像组成,通常具有固定的间隔。电影也可以被视为具有每秒 24 或 25 帧(fps)固定间隔的时间序列。时间序列的处理主要关注两个领域:随时间的强度波动和背景减少及标准化。强度波动已在之前的章节中介绍,其中我们讨论了漂白校正。在下一节中,我们将探讨标准化时间序列数据的方法。
标准化时间序列数据
标准化时间序列数据将有助于进一步分析,因为它提供了对基线强度的校正。很多时候,时间序列的目标是观察强度或随时间的变化。标准化将产生相对于静息或基线状态更干净的时间序列数据。一个非常简单的标准化方法是计算ΔF 相对于 F0(dFF0)。这个指标的基础是基线荧光在不同时间序列之间可能不同,但相对于基线的强度变化是相似的。它使用以下公式进行计算:

分子是当前帧(Ft)与基线(F0)之间的差值。基线是前n帧的平均值。dFF0的值大于 1 表示信号相对于基线增加,而小于 1 表示相对于基线减少。这种计算只能在时间序列的测量值上执行(在 Excel 或 MATLAB 中),但你也可以直接转换时间序列。我现在将向您展示如何在 ImageJ 中使用 Z 投影、图像复制和图像计算器来完成这项操作。
要开始,我们将打开timeseries_events.tif图像,该图像可在本书的在线资源中找到。这是一组细胞中囊泡的时间序列,当细胞用电极刺激时,囊泡会被运输并融合。它包含两个通道:一个带有红色荧光标记,另一个带有绿色荧光标记。红色标记在囊泡融合之前始终是荧光的,此时它会消失。绿色标记在货物位于囊泡内时不是荧光的,但一旦融合,它就会变亮。为了开始处理,我们首先想要将通道分成两个不同的时间序列。为此,选择时间序列,转到Image | Color | Split Channels以生成两个时间序列:每个通道一个。我们将使用split channels命令选择绿色通道,其标记为C1_timeseries_events.tif。
我们现在可以开始创建dFF0时间序列的第一步:创建基线帧。我们将转到Image | Stacks | Z Project…并设置方法为Average Intensity和Stop slice:为5。我们在这里所做的就是创建前五帧的平均值。这实际上通过平均减少了单个帧中的噪声,同时保留了第一帧中存在的明亮物体。让我们将生成的图像重命名,以便稍后更容易识别。右键单击平均图像,从上下文菜单中选择Rename…。将图像重命名为F0,以便稍后更容易选择。
对于下一步,我们将创建ΔF 图像。正如本节开头所解释的,这个图像是原始图像减去基线图像。为了得到这个图像,我们将通过ImageJ菜单中的Process | Image Calculator来使用图像计算器。选择原始时间序列为Image1和F0图像为Image2。然后,将方法设置为Subtract。确保已选中Create new window选项。
注意
当其中一个图像是堆栈或时间序列,而另一个是单个帧时,图像的顺序非常重要。如果你希望修改每个切片或帧,堆栈始终需要设置在Image1位置。对于减法,这通常是显而易见的,但对于乘法,从数学角度来看操作顺序并不重要(A × B 等于 B × A)。然而,如果你将时间序列或堆栈放在Image2上,而单个帧放在Image1上,则只有当前切片或帧用于计算!
我们现在有了ΔF 堆栈,所以让我们重命名它以使其更容易识别。右键单击新的时间序列,选择重命名…,并输入deltaF作为新名称。
现在,我们可以创建最终的时间序列,并将其归一化到基线。请注意,deltaF序列本身已经比原始时间序列有所改进,因为它已经校正了初始静态背景。要创建dFF0图像,我们将再次使用图像计算器。这次,我们将选择deltaF作为Image1,F0作为Image2,并选择除法操作。选择创建新窗口和32 位(浮点)结果选项。
注意
这次,32 位结果选项很有用。正如我们之前提到的,在计算定义中,我们期望结果在 0 到无穷大之间。这在数学符号中表示为[0, ∞]。这意味着任何值,包括 0 和无穷大,都在可能值的范围内。当在计算过程中未选择此选项时,所有低于 1 的值都将被舍入到 0,这些事件的信息将丢失。请注意,对于这里使用的示例,我们希望看到的事件值将大于 1。因此,在这种情况下,这并不重要。
新图像现在是 dFF0 图像,它已经校正了基线并归一化到初始基线强度。以下图像显示了这种归一化的效果(第二行),与原始图像(第一行)相比:

可以看到的最明显差异是,在帧300之前的图像几乎是黑色的,这表明相对于基线情况没有发生任何事情。在帧300及以后,不同位置的信号增加非常明显,这表明这些位置的信号已经增加。
摘要
在本章中,我们研究了不同类型图像的处理过程。我们探讨了可能导致图像损坏并降低其质量的噪声来源。你学习了如何应用不同的校正方法来修复这些问题。然后,我们查看针对 Z 堆栈和时间序列的特定处理步骤。
在下一章中,我们将看到如何将像素分成不同的组,以及如何清理和过滤结果以进行进一步处理。
第四章:使用 ImageJ 进行图像分割和特征提取
上一章探讨了处理图像以查看和纠正采集中的缺陷。本章将介绍分割图像和提取与处理和分析相关的特征的技术。本章将涵盖以下主题:
-
图像分割
-
形态学处理
-
图像过滤和卷积
-
特征提取
图像分割
在图像分析的许多步骤中,将图像分成两个独立的(非重叠的)部分非常重要。这些部分通常被标记为背景和前景。一般来说,当我们分析图像时,背景是我们不直接感兴趣的图像部分。我们通常将分析限制在被认为是前景的图像部分。这种将图像分成两个部分的过程称为分割,主要是基于像素强度。如果您希望计数和测量特定类型的多个独特对象或测量单个复杂对象的强度,同时排除背景,这是很重要的。
图像阈值化
为了将图像分割成背景和前景,我们将设置一个阈值值。低于此阈值的值将被分类为一组,而值更高或等于阈值的像素将被分类为另一组。一般来说,荧光图像的背景包含接近黑色的值(即,深色背景),而明场图像的背景值更接近白色(浅色背景)。阈值化的输出是一个在 ImageJ 中称为掩模的图像,它是一个二值图像。其像素只有两个值(0 和 255)。
我们首先将探讨如何在灰度图像上执行基本的阈值化。之后,我们将探讨对彩色图像进行阈值化的可能性。这两种图像类型之间的区别在于,彩色图像没有一种简单的方法来设置阈值。每个像素包含三个值(红色、绿色和蓝色),而单个阈值值通常不能以有用的方式分割图像。
阈值化灰度图像
我们将从一个示例图像中获取一个灰度图像并对其进行分割。对于这个例子,我们将使用Blob图像。如果您想测量每个单独的 Blob 的大小以及获取图像中 Blob 数量的计数,阈值化将是第一步。请注意,对于像这个例子这样的小图像,计数可以手动完成。然而,如果您需要为大量图像执行此操作,这种手动计数方法将非常繁琐。
要设置阈值,请转到图像 | 调整 | 阈值…或按Ctrl + Shift + T。阈值对话框将打开,并带有几个选项:

对于荧光图像,需要选中暗背景复选框,而对于明场图像,则需要取消选中(除非您使用暗场照明方法)。可用的方法可以在左侧的下拉列表中设置。默认方法基于IsoData方法。IsoData方法根据以下程序确定阈值值:
-
为阈值 T 设置一个初始值
-
根据阈值 T 的值计算背景 (BG) 和前景 (FG) 像素的平均强度
-
如果步骤 ii 中 BG 和 FG 的平均值不等于 T,则增加阈值值 T 并重复步骤 ii
更多有关阈值化方法的信息和参考资料,请参阅 Fiji 网站上的fiji.sc/Auto_Threshold#Available_methods,以获取概述。右侧的下拉列表提供了显示阈值化效果的选择。当选择红色时,前景选择以红色显示,而背景保持为灰度。
一旦设置了阈值,您可以通过在阈值窗口中按应用或通过转到编辑 | 选择 | 创建蒙版来创建二值图像。前者将修改您的原始图像,而后者将打开一个包含阈值化图像的新窗口。原始图像中的红色部分(即高于阈值的值)现在是白色,而原始图像中的非红色部分(即低于阈值的值)现在是黑色。有时,阈值可能并不完美,在信号甚至不存在的地方有间隙或孔洞。您将在形态学处理部分学习如何处理这些问题。此过程的三个阶段如下面的图像所示:

左侧面板中的图像是原始图像。中间面板显示了带有红色前景区域的自动阈值。右侧面板显示了基于阈值创建的结果蒙版。
阈值化彩色图像
如前所述,彩色图像的分割更复杂。当谈到彩色图像时,区分 RGB 图像和多通道堆叠非常重要。后者可以使用上一节中描述的技术很好地进行阈值化。多通道堆叠可以看作是给定了特定 LUT 以呈现颜色的单个灰度图像。另一方面,RGB 图像稍微复杂一些。如果图像只包含红色、绿色或蓝色的像素,则可以将图像转换为多通道图像。
要分割具有更多颜色的 RGB 图像,您需要将图像转换为不同的颜色空间。要基于颜色选择前景,HSB 颜色空间更为方便。正如我们在第二章中看到的,使用 ImageJ 的基本图像处理,HSB 图像中的颜色信息是单独的通道,以灰度值编码。当您想在 ImageJ 和 Fiji 中设置 RGB 颜色图像的阈值时,会自动打开阈值颜色对话框:

默认情况下,它以 HSB 颜色空间打开,顶部图表显示了色调通道的分布。下方的两个滑块指示您希望选择的颜色。在这种情况下,选择了橙色。第二个面板显示了饱和度的控制。由于滑块位于最右侧,我们只选择了明亮的橙色。最后,底部第三个面板显示了亮度控制,亮度值从中等水平开始,覆盖了广泛的亮度范围。这个例子展示了如何选择特定范围的色彩。在这种情况下,阈值被设置为选择小丑样本图像中的头发:

如您所见,阈值并不完美。脸颊和鼻子附近的小区域也位于阈值内。此外,该区域中还有间隙,尤其是右眼周围和图像右上角。
阈值方法列表具有与标准 ImageJ 阈值对话框相同的方法,并且它仅在亮度通道上工作。原始按钮类似于灰度阈值对话框中的重置选项。选择按钮将阈值化区域转换为选择。样本按钮将使用图像的一部分来生成基于该区域的色调、饱和度和亮度通道的阈值。
形态学处理
在将图像分割成两个组件之后,您将得到一个掩码或二值图像。从示例中可以看出,这些掩码并不总是适合直接测量。图像中的不完美可能导致物体之间出现间隙或结构中的小不连续性。此外,某些区域可能被检测为前景,而实际上它们并不是真正感兴趣的对象。您可以通过将缺失的像素转换为白色或黑色来手动纠正这一点,分别包括或排除它们。在某些情况下,这可能是最可能的解决方案。然而,在许多情况下,有一些处理步骤可以系统地解决这些问题。这些步骤被称为形态学处理,我们将在下一节中更详细地探讨。
形态学算子
ImageJ 支持两种主要的形态学处理运算符:腐蚀和膨胀。它还具有填充孔洞、骨架化和分水岭处理二值图像的功能,这些将在后面的章节中讨论。这些功能将通过一些基本示例在即将到来的章节中进行解释。
腐蚀和膨胀
首先,我们将查看基本的运算符腐蚀和膨胀。腐蚀运算符会取一个前景像素(FG),并查看 3 x 3 邻域内的周围像素。根据 FG 像素的数量,像素将被改变为背景像素(BG),或者它保持为 FG 像素。膨胀运算符则相反。ImageJ 根据二进制选项确定像素是否改变,这些选项可以通过访问处理 | 二值 | 选项…来设置:

迭代次数决定了运算符重复的次数,计数决定了用于确定像素是否切换的阈值所使用的像素数量。EDM 输出决定了距离映射函数的结果写入的位置。当选择覆盖时,距离映射会覆盖遮罩图像中的像素。腐蚀时填充边缘决定了当像素位于图像边缘时是否进行腐蚀。当选择时,图像边缘将不会有腐蚀。
对于以下示例,我将假设迭代次数设置为1,计数设置为1,并且未勾选黑色背景。
-
在 ImageJ 中打开
4909_03b_binary.tif图像。它可在 Packt 网站上找到。 -
使用默认方法,通过顶部滑块设置图像的阈值使用
0,底部滑块使用75。不要勾选深色背景。 -
选择编辑 | 选择 | 创建遮罩以生成一个新的图像,或在阈值对话框中按应用以覆盖原始图像。
-
最后,选择遮罩图像,并按Ctrl + Shift + I来反转图像,使其具有白色背景。你现在应该得到以下结果(原始图像在左侧,遮罩在右侧):
![腐蚀和膨胀]()
当你仔细观察被遮罩的图像时,你会注意到存在一些小问题。最值得注意的是,二进制和ImageJ中的字母a被分成三个不相连的部分。此外,字母p和g并不完全完整,有一个断裂但并未完全断开。对于人类来说,这并不是一个大问题。我们可以在脑海中轻松填补这些空白并阅读文本。然而,另一方面,计算机可能更难尝试解读文本。我们现在将探讨二进制运算符对这一遮罩的影响。你也会看到这如何解决我们碎片化字母的问题。
-
选择蒙版图像,然后转到处理 | 二值 | 选项…以打开选项对话框。现在我们有了蒙版图像,将显示一些额外的选项,最值得注意的是执行下拉菜单和预览复选框。
-
使用放大工具或键盘上的+键放大字母a。
-
从执行下拉菜单中选择腐蚀,并勾选预览复选框,但不要按确定!
在预览模式下,你会注意到整个蒙版变成了白色,当你选择了腐蚀操作时,文本完全消失了。当你增加计数字段中的值时,你将开始注意到文本的部分开始恢复。当值为3时,一些像素可见,而值为7或8则几乎保留了所有文本。当值设置为8时,腐蚀操作的唯一受害者是字母a的孤立像素。所有其他像素保持完好,但这个孤立像素被移除了。这是腐蚀操作最常用的应用之一——移除由图像中的噪声引起的孤立单个像素。
注意
当使用腐蚀时,可以移除孤立像素,但整个蒙版会变得更小,从而减少了我们想要测量的区域。在腐蚀操作之后直接使用膨胀(或使用开运算),我们可以在保留我们想要测量的区域的同时移除孤立像素。一旦由于腐蚀而丢失前景像素,它就永远无法返回,无论你使用多少次膨胀!
现在,从执行下拉菜单中选择膨胀,将计数设置为1,看看效果如何。当你使用膨胀操作时,文本会变得更粗,但它也会填充字母中的间隙。这种结果更有用。然而,这里有几个问题。字母g的底部尾巴以及字母e的开口现在被填充了。通过将计数增加到2,这个问题得到了改善,字母e以及字母g的尾巴再次打开。当计数为2时,膨胀修复了碎片问题。然而,我们的字母现在变得非常粗,一些字母合并了。看看单词ImageJ中的eJ。字母e的尾巴直接连接到字母J的尾巴。我们现在想采取两个步骤。首先,我们想要膨胀蒙版以填充间隙,然后,我们想要腐蚀蒙版以去除连接的字母。对蒙版连续执行操作可以完成这种组合。首先,我们将膨胀蒙版,然后我们将腐蚀结果。然而,还有一个特殊功能可以按此顺序执行这两个步骤,称为闭合。如果你想按相反的顺序执行步骤(先腐蚀然后膨胀),可以使用开运算。
当你在下拉菜单中选择闭运算选项时,你可以看到这个动作的结果。结果是 OK,但并不那么出色。在这种情况下,结果并不那么好的原因是我们在每个步骤中使用了不同的计数值。膨胀操作在我们使用2时效果最好,而腐蚀操作在7或8时效果最好。对于这个例子,最好以每个步骤中特定的计数值连续执行膨胀和腐蚀操作。在以下图像中,闭运算使用了2的计数值,而膨胀和腐蚀的连续操作分别使用了2和5(左侧图像是原始掩码):

如中间和右侧面板所示,两种方法都有其优点和缺点。闭运算(中间面板)填充了字母e,并且仍然有一个孤立的像素是字母a的一部分。然而,字母本身仍然有很好的细节。手动连续膨胀/腐蚀步骤(右侧面板)保留了字母e中的孔以及字母g的细节。然而,字母a的细节不太明显。具体来说,衬线(字母a右下角的小钩)完全丢失了。
骨架化和分水岭
在使用膨胀、腐蚀、开运算和闭运算处理掩码后,我们可能希望将掩码简化到最基本特征。我们之前分割的字母的核心是由笔画构成的。每个字符由一组不同方向的笔画组成,这些笔画共同定义了字符。在 ImageJ 中,我们可以使用骨架化功能来重新创建这些笔画,该功能可以通过在菜单中选择处理 | 二值 | 骨架化来找到,或者通过在二值选项对话框的做下拉菜单中选择它。骨架化会检查每个像素的邻居,如果像素被其他前景像素包围,则会移除该像素。这导致将掩码简化为单像素宽度的掩码。
将闭运算(左侧面板)和连续膨胀/腐蚀(右侧面板)操作的结果应用于掩码,结果如下:

闭运算操作的结果(左侧面板)并不令人满意。字母e无法识别,看起来更像字母c。连续的膨胀/腐蚀操作(右侧面板)由于骨架化操作而略有改善。尽管字母看起来有点滑稽和摇摆,但所有重要的笔画都存在。
分水岭功能将分离接触的物体。我们将通过使用连通区域样本图像来查看此操作的效果。你也可以将其应用于文本示例。然而,文本示例中的问题是物体需要连接而不是分离。
-
从样本图像中打开连通区域图像。
-
使用默认方法设置阈值,不勾选暗背景框,然后点击应用以创建掩码。
-
现在,从菜单中选择处理 | 二值 | 分水岭。
结果将如下所示,左侧为原始掩码,右侧为分水岭操作的结果:

如所示,四个连通区域被分割成两个独立的对象。此操作寻找被挤压的区域。当一个物体在两个较厚的部分之间有一个狭窄的部分(类似于数字 8 的中间),它将沿着狭窄部分被分割。然而,请注意,这并不适用于某些连通区域(用蓝色矩形表示)。当轮廓没有挤压时,分水岭算法不会分割物体。如果你知道物体可以重叠,并且想要量化物体的数量时,这将非常有用。然而,如果你想要测量物体的大小或面积,可能会遇到问题。因为重叠区域无法准确测量,重叠物体的测量值将低估实际大小。这个问题可以通过假设物体具有规则形状(如椭圆形)来解决,但在许多情况下这可能不成立。在 ImageJ 中,可以使用颗粒分析器使用这个后者的假设,这将在第五章,使用 ImageJ 的基本测量中讨论。解决这个问题的最好方法是确保重叠量减少,这可能需要改变你的样本制备或采集。
图像滤波
上一节探讨了使用阈值分割前景和背景的方法。它还探讨了使用形态学算子得到适合分析的结果的方法。形态学算子被用来通过去除孤立像素来清理阈值的结果。在大多数实际应用中,这些孤立像素是由于图像采集系统中的噪声效应造成的。一些噪声可以使用上一章中描述的技术去除,但这可能不会去除所有噪声。在本节中,我们将探讨使用过滤器去除噪声并准备图像以创建掩码的方法。滤波可以是插入在阈值和形态学处理之前的一个步骤。如果你的图像对比度很高且噪声水平极低,这可能不是必需的。然而,这种情况相对罕见。
根据用于滤波的域类型,滤波可以分为两类。图像可以在两个不同的域中看到:空间域和频域。对人类来说最易识别的是空间域。这是我们通过相机认识到的图像。空间中的每个位置都有一个值,由具有不同值的紧密排列的位置区域组合形成的图像。如果所有值都相同,图像将呈现为单一颜色或灰度均匀。在数字图像的情况下,位置由构成图像的像素指定,值以灰度值或 RGB 值表示。
频域对人类来说不太容易识别。频域中的图像是通过值的改变率或频率来表示的。人类通过光波的波长来识别频率。频率较高的光看起来是蓝色/紫色,而频率较低的光看起来是橙色/红色。然而,在图像处理中,图像的频率是由图像中像素强度变化的方式决定的,而不一定是像素的颜色。我将从频域滤波开始,因为这更为复杂。请注意,图像处理的大部分滤波都是在空间域中进行的,并且效果非常好。
频域滤波
图像滤波基于 1822 年由约瑟夫·傅里叶描述的变换技术。这种变换将一个域中的数据转换到另一个域,然后再转换回来。对于图像处理,这种变换是从空间域到频域的转换。空间域考虑点位于空间中,要么是平面(2D),要么是体积(3D)。每个点的位置都有一个强度值,对于大多数图像,这个值会随着不同位置而变化。强度沿一个维度变化的速率决定了频率。看看这张人工图像:

如果我们观察图像宽度方向以及中间图像高度方向上的强度轮廓,我们会得到以下结果(水平轮廓在左侧,垂直轮廓在右侧):

从这些图中很明显可以看出,强度变化率存在明显的差异。水平轮廓(左侧)显示了强度随距离的快速变化,而垂直轮廓(右侧)则完全没有变化。另一种描述方式是,水平轮廓上的频率较高,而垂直轴上的频率较低。
傅里叶变换将计算空间域中的频率,并将它们作为x和y方向上的频率绘制出来。变换的思想基于这样一个事实,即任何信号都可以描述为不同频率的谐波函数(即正弦和余弦函数)的(无限)和。这些频率由正弦和余弦的系数表示,ImageJ 以灰度值的形式在图像中显示这些系数。我们可以通过从菜单中选择处理 | FFT | FFT来获得人工图像的傅里叶变换,即快速傅里叶变换(FFT):

图像的中心(即原点)的频率为 0,而通过原点的水平线代表图像的x轴上的频率。象限中的值决定了图像对角线上的频率。靠近图像中心的值代表低频率,而靠近边缘的值代表较高频率。由于图像沿x坐标的变化只有频率的变化,因此变换后的图像只显示垂直线。如果模式是斜的,变换后的图像中的线条也将是斜的。
注意
变换后的图像中线条的虚线外观是由输入图像不是正方形造成的。宽度是 512,但高度只有 128 像素。如果图像是 512 x 512 的正方形,变换后的图像将只显示通过原点的x轴上的一行点。如果你将样本图像的高度减半,虚线将大约变为两倍长。
当我们使用 FFT 图像作为输入时,我们可以通过从菜单中选择处理 | FFT | 逆 FFT来创建原始图像:

注意,由于我们使用了 FFT 和立即的逆 FFT,实际上我们没有应用任何滤波。变换前后的图像是相同的。这是变换的一个非常理想的特点,因为这意味着变换是无损的。在过程中没有丢失任何信息。要实际滤波图像,我们需要通过修改变换图像中的像素值来修改变换后的图像。
要应用一些(粗略的)滤波,我们将采取以下步骤:
-
选择变换后的图像。
-
从菜单中选择编辑 | 选择 | 指定…并输入以下值:宽度为
255,高度为255,X 坐标为0,Y 坐标为0。然后,按确定。 -
通过从菜单中选择图像 | 颜色 | 颜色选择器或按Ctrl + Shift + K键在键盘上打开颜色选择器。
-
确保通过点击颜色选择器右下角的小黑白方块图标将前景设置为黑色。
-
现在,通过转到编辑 | 填充或按Ctrl + F来用黑色填充我们指定的选择区域。
-
重复步骤 2 和 5,但现在,指定选择区域的X和Y坐标为
257。 -
最后,从菜单中选择处理 | FFT | 逆 FFT以生成滤波后的图像。
如果你遵循了指示,你的 FFT 图像将如下所示:

逆 FFT 图像将如下所示:

如在逆 FFT 图像中所示,经过操作前后存在显著差异。例如,垂直方向上的频率不同。现在,每根条形图从上到下强度都会发生变化。尝试相同的程序,但这次在步骤 2 中,使用以下参数指定选择区域,并跳过步骤 6:
-
宽度:
64 -
高度:
512 -
X 坐标:
272 -
Y 坐标:
0
在用黑色填充选择区域并计算逆 FFT 后,图像将如右面板所示。你已特别从频率域中移除了一小部分频率。在计算逆 FFT 后,你将得到以下结果(左上角缩放区域):

在左侧,你看到的是原始图像,在右侧是滤波后的图像。由于选择和移除的区域涉及低频,高频保持不变,导致沿水平轴的强度值变化更大。
由于这个例子非常人为,这里的结果不一定适用于分析。然而,如果你有一个被高频强度变化(例如,成像噪声)损坏的图像,你知道你必须移除 FFT 变换边缘的频率。另一方面,如果你有一个强度变化的缓慢梯度(例如,不均匀照明),你需要移除 FFT 变换中的低频。使用黑色移除频率,你正在创建一个排除你选择覆盖的频率的滤波器。如果你用白色填充选择区域,你会包括所有被选择覆盖的频率。在下一节中,我们将探讨空间域中的滤波,这稍微更容易应用。
空间域图像滤波
在空间域中进行滤波涉及使用一个滤波器,通常称为核。这个滤波器通过一种称为卷积的方法转换每个像素。卷积包括取一个中心像素及其周围的小数组像素(通常是 3 x 3),并将这些像素的强度与核中定义的权重相乘。乘积的总和将成为中心像素的新强度。在以下示例中,有一个图像的一部分(左侧),核(中间),以及卷积的结果(右侧):

中心像素(橙色突出显示)及其 3 x 3 邻域内的周围像素与核(中间)相乘。卷积的结果显示在右侧。中心像素的值原本是 128,但卷积后变为 78。在这个示例中显示的核是一个简单的平滑滤波器(也称为方框滤波器)。这个滤波器的主要效果是平均像素,导致图像模糊。以下图像是“Boats”样本图像的细节,在卷积前(左侧)和卷积后(右侧):

当你将滤波器大小更改为 7 x 7 时,平滑的效果将会更强烈,因为更多的邻域像素将影响中心像素的值。当使用 7 x 7 大小的方框滤波器时,每个权重将等于 1/49。相同图像的结果将如下所示:

注意到滤波几乎完全平滑了字母,使它们难以辨认。方框滤波器充当低通滤波器——图像中只有低频部分会保留。这是由于快速变化的强度会被方框滤波器比低频更强烈地平滑掉。尽管这种滤波发生在空间域中,但效果也会在频域中反映出来。
要重新创建前面的图像,请在样本图像中的“Boats”图像上按照以下步骤操作:
-
从菜单中选择处理 | 滤波器 | 卷积…,并在打开的对话框中的文本字段中删除所有内容。
-
输入三个用空格分隔的 1,然后按回车。重复此操作两次。
-
确保选中了归一化核复选框,然后按确定。
-
现在图像看起来稍微不那么清晰了,因为它已经与一个 3 x 3 的方框滤波器进行了卷积。
如果你想要使用一个 7 x 7 的方框滤波器进行卷积,只需输入七行,每行七个用空格分隔的 1,然后在新建的“Boats”图像上重复这些步骤,以查看核大小的影响。
注意
当将核应用于已经卷积过的图像时,效果会比图像尚未卷积时更大。当连续两次使用 3 x 3 箱形滤波器时,效果等同于运行权重为每像素 1/81 的 3 x 3 箱形滤波器(1/9 * 1/9)。
使用核进行滤波的结果取决于您指定的权重值和核的大小。通常,根据其权重的总和,可以将核分为两种类型。当一个核中权重的总和为 1 时,该核被称为归一化核。归一化核的优势在于卷积的结果不会超过图像位深允许的最大像素值。当在卷积对话框中勾选归一化核复选框时,ImageJ 将自动处理归一化。非归一化核可能会出现夹断伪影。当一个核的权重总和超过 1 时,卷积的结果可能会超过最大允许值(即 8 位图像的 255)。当这种情况发生时,变换后的值将被夹断在最大值。这种夹断可能会导致如白色像素块等伪影。
箱形滤波器是一个非常简单的滤波器,但它不会在图像中区分任何特征。它在所有方向上均匀平均。其他滤波器实际上可以增强图像中的某些特征。这类滤波器的例子是墨西哥帽滤波器。这个滤波器强调中心像素相对于周围像素:

墨西哥帽滤波器形状有点像墨西哥草帽,因此得名。它使得高对比度区域变亮,而均匀强度的区域变暗。应用于Boats图像,结果如下:

立即引人注目的是字母边缘被极大地强调。这很合理,因为对比度相对较强。这些是黑色字母,背景主要是均匀的浅灰色。唯一不清晰可辨的边缘是字母接触的点以及绳索隐藏字母部分的地方。你可以想象这个过滤器也可能对文本示例和前面提到的 blob 分割效果很好。它基本上作为一个高通滤波器工作。只有强度变化快的区域被强调,而强度变化慢的区域(即低频)被减少。
除了手动输入核权重外,ImageJ 和 Fiji 还有一些常见的滤波器核,可以通过访问处理|滤波器来获取。最常用的两个滤波器核包括高斯模糊…和均值…滤波器。后者与方滤波器相同。前者类似于墨西哥帽滤波器。然而,它不使用核中的负值。高斯模糊滤波器平滑图像的方式与方滤波器相同,但它以更渐进的方式完成。高斯模糊的优势在于,当你应用它时,可以产生更少的伪影。滤波器在频域的响应也更好,这使得结合空间域和频域滤波成为可能。
接下来,我们将探讨一些可以用于检测图像中可能对处理相关的特定特征的算子。这些算子也使用卷积,但它们与前面描述的滤波器具有不同的特性。
特征提取
如我们在前面的章节中看到的,可以使用滤波器通过滤波器隔离不同的频率。通过将图像与墨西哥帽滤波器卷积,可以保留高频,而使用方滤波器则产生相反的效果。本节中滤波器与上一节中滤波器的区别在于特异性。墨西哥帽滤波器没有方向偏好。当存在具有尖锐对比度(强度快速变化)的边缘时,滤波器有强烈的效果。然而,有时你只对特定类型的边缘感兴趣。假设我们只想检测垂直边缘。墨西哥帽滤波器将给我们所有方向的边缘,而不仅仅是垂直的。这将是下一节的主题。
边缘检测
要检测仅垂直的边缘,我们需要创建一个强调垂直方向的像素的核。以下核可以检测不同方向的边缘:

要执行Sobel 边缘检测,您可以使用处理菜单中的查找边缘命令。此命令将在图像上运行水平和垂直的 Sobel 核。
最后,还有用于边缘检测的Canny 过程,它涉及五个步骤。此过程由 John F. Canny 开发,包括以下步骤:
-
应用高斯平滑以去除噪声。
-
使用边缘检测在图像中检测梯度。
-
使用类似墨西哥帽的核通过卷积细化边缘。
-
应用两个不同的阈值来确定弱边和强边。
-
移除那些未连接到强边的弱边。
前三个步骤涉及依次使用不同的核进行平滑、边缘检测和边缘细化。请注意,如果图像因噪声而退化,则第一步是必需的。如果对比度高且没有噪声,则可以跳过这一步。这一步也是整个流程中最薄弱的点。噪声和边缘都是高频信号的形式,高斯滤波器对两者进行同等程度的平滑。如果存在噪声,那些在保留边缘的同时专门减少噪声的技术应该会显示出显著的改进。
摘要
在本章中,我们探讨了将图像分离为前景和背景的方法。我们看到了在灰度和彩色图像中设置阈值的不同方法。我们应用了空间域和频域中的滤波来帮助清洁图像并提取边缘以供进一步处理。所有这些步骤都将帮助我们,当我们希望测量图像中的对象时,这是下一章的主题。
第五章. 使用 ImageJ 进行基本测量
在上一章中,我们看到了如何执行一些预处理步骤,这些步骤为测量和分析图像数据做好了准备。在本章中,我们将更详细地查看 ImageJ 中可用的测量系统。你还将学习如何创建一些运动和动态的视觉表示。本章将探讨以下主题:
-
选择和区域
-
ROI Manager
-
柱状图和线形图
-
区域和线选择
-
半定量共定位
-
粒子分析
ImageJ 中的选择和区域
我们将首先查看 ImageJ 中可用于选择感兴趣区域(ROIs)的工具。如果你只想处理图像的一小部分,这些工具可能很有用。ROIs 是 ImageJ 中的一个非常重要的元素,还有一个专门的管理器来处理 ROIs:ROI Manager。可以通过转到分析 | 工具 | ROI Manager…来打开它,这将打开以下窗口:

在左侧,有一个将包含 ROIs 的列表,而在右侧,有几个按钮将对 ROIs 执行某些操作。右下角底部的复选框允许用户一次性查看所有区域(显示所有),而标签复选框则在图像中显示区域标签。
ImageJ 支持不同类型的区域。它们可以分为两大类:区域选择和线选择。第三种单一类型是点 ROI,它只有一个成员。当用于测量时,可测量的参数略有不同。面积只能用区域类型的 ROI 来测量,而角度只能用线选择来测量。
首先,我将讨论 ImageJ 支持的几种常见选择类型,然后我们将应用它们进行测量。
区域选择
ImageJ 中的区域选择包含不同类型,具有不同的属性。以下是在 ImageJ 中可用的类型:
-
矩形
-
椭圆
-
多边形
-
自由手绘
矩形通常用于选择裁剪图像的区域或用于矩形对象。如果你的图像包含更多有机形状,如椭圆形或多边形区域,则更合适。这些类型可以通过在 ImageJ 程序的工具栏中选择适当的工具添加到图像中。然后,你可以左键单击并拖动鼠标来包围你想要选择的区域,然后释放鼠标按钮。设置选择后,你可以按 ROI Manager 上的添加按钮(或按Ctrl + T)将区域添加到 ROI Manager。
注意
如果你转到编辑 | 选项 | 杂项…时未取消要求控制/命令键用于快捷键选项,则只需按字母T即可将 ROI 添加到管理器。
当区域添加到区域管理器时,可以将其保存到文件中以便以后保存。当多个区域添加到区域管理器,并在尝试保存区域时选择其中一个时,只有选定的区域将被保存。如果你希望一次性保存所有区域,请按取消选择以取消选择所有区域。或者,你可以在选择更多 | 保存从区域管理器之前,使用Ctrl + A选择所有区域。
注意
单个区域将保存为具有.roi扩展名的文件。多个区域将保存为.zip文件中的区域集。这个压缩归档包含单个.roi文件,每个选择一个。在 Windows 上,默认隐藏已知文件类型的扩展名。因此,要查看扩展名,你可能需要取消选中文件夹选项中的隐藏已知文件类型的扩展名选项。
在添加额外的区域时,选择区域管理器中的显示所有复选框可能很方便。这将显示列表中当前的所有区域。在区域管理器中点击区域可以设置活动区域。它总是以你在选项中设置的颜色显示(参考,第一章, 使用 ImageJ 入门),在角落有小白方块:

这些小方块是控制点,可以用来移动和调整区域的大小。如果你通过调整大小或定位修改区域,按下区域管理器上的更新按钮将更新列表中的区域。要调整大小,点击并拖动其中一个方块到新位置,然后释放鼠标按钮。如果你在拖动手柄时按住Shift键,形状将变为等宽等高的正方形或圆形。如果你在点击控制点时按住Ctrl键(或在 Mac 上按Cmd键),区域将在中心周围等宽等高增长。如果你在调整大小时按住Alt键,另一侧的手柄将保持在固定位置,同时调整区域大小,保持长度与宽度的比例相等。如果你按住Alt键并创建一个与先前区域重叠的区域,将形成两个区域重叠部分的减法。相反,如果你在创建与先前区域重叠的新区域之前按住Shift键,你将创建两个区域重叠部分的并集:

在拖动的同时按下两个键的组合也可以实现同时调整大小效果。要移动一个区域,将光标移至区域内部,然后点击并拖动区域到新位置。在点击和拖动区域之前,确保光标形状为箭头——而不是手形或十字准星。对于小区域,你可能需要放大才能移动区域。如果光标接近控制点,它将变为调整大小模式。
一旦放置并添加到 ROI 管理器中,在 ROI 管理器中点击测量按钮或使用Ctrl + M键盘快捷键将测量区域。要选择要测量的参数,请转到分析 | 设置测量…以选择参数:

平均灰度值是区域内的平均强度,而面积则测量区域的面积,单位为图像单位。一旦你在 ROI 管理器中点击测量,就会打开一个包含测量结果的新窗口。此窗口显示结果。一些不同的测量将在面积选择和测量部分中解释,我们将使用它们从我们的图像中提取有用信息。
线选择
线选择包含以下类型:
-
直线
-
分段线
-
角度工具
ImageJ 以类似的方式处理这些选择类型。然而,它们可以用于不同的测量。最常用于线段的函数之一是沿线或分段线绘制轮廓图。另一个选项是创建光栅图。这两个选项将在接下来的章节中演示。
点选择
第三种选择类型只包含一个工具:点工具。它只选择一个像素,主要用于计数或标记物体的中心点。点选择的优点是只需单击一次即可放置。然而,可以获得的测量结果仅限于X和Y坐标以及强度。
基本测量
我们现在将探讨一些可以用来从数据中测量特定参数的技术。对于这些测量,我们将使用 ROI 管理器和几种不同类型的区域来选择和测量强度、速度和其他有趣的事物。除了测量之外,区域还可以用于与处理和图像处理相关的其他目的。选择的一个有用应用是,它们可以用来限制某些处理步骤仅应用于所选区域,而未选择的像素不受影响。还将演示一些这些应用的例子。
面积选择和测量
我们将使用面积选择开始一些基本测量。我们将使用这些来测量一些基本参数,如面积、周长(或圆周),等等。我们将从最基本的面积选择开始:矩形。
矩形选择对于裁剪图像区域非常有用。通过减小图像的大小,可以减少所需的内存以及复杂算法的处理时间。矩形选择的另一个良好用途是将较大图像的处理限制在特定区域。一些 ImageJ 算法和工具可以在活动选择内工作。我们将在第九章 中看到一个此类应用的例子,即 创建用于分析的 ImageJ 插件。由于矩形的简单性,如面积和周长之类的测量实际上并不相关。你可以非常容易地使用宽度和高度来计算矩形的面积和周长。因此,我们将关注矩形选择的一些更有用的应用。
首先,让我们使用矩形选择来通过反转其灰度值来修改图像的一个小部分。为了开始一个例子,从样本图像中打开 Blobs 图像。我们将使用以下步骤来反转单个区域的查找表(LUT):
-
使用矩形选择工具在图像中选择一个区域。
-
选择一个区域后,按 Ctrl + Shift + I 来反转查找表(LUT)。
-
使用 Ctrl + Shift + A 来清除所有选择,并再次使用 Ctrl + Shift + I 来反转查找表(LUT)。
在这个小练习中,我们只关注修改所选像素,而选择区域外的像素保持不变。如果没有像素被选择,反转 LUT 命令将对所有像素起作用。这是一个如何使用面积选择来限制处理仅限于所选像素的例子。同样的方法也适用于其他面积选择。你也可以使用这种方法来突出显示图像的特定部分,使其突出。例如,在下面的 kymograph 示例中,我们可以通过创建一个方形选择并仅反转该时间段内的 LUT 来显示在刺激期间获取的线条。
椭圆形选择
ImageJ 有两种用于圆形形状的选择类型:椭圆和椭圆形。这两种类型之间的区别很微妙,但椭圆形选择只能沿 x 或 y 轴进行形状调整。另一方面,椭圆可以自由旋转。要创建一个圆形,在创建椭圆形选择时按住 Shift 键,以强制 ImageJ 创建具有相等宽度和高度的圆形。椭圆形选择的另一个重要属性是其形状描述符。ImageJ 在其测量中报告的形状描述符是圆形度(Circ.)、圆度(Round)、纵横比(AR)和密度(Solidity)。圆形度定义为如下:

在这里,A 代表面积,而 C 代表周长。圆度定义为如下:

在这里,主轴是椭圆的最大直径。长宽比是椭圆的主轴和副轴之间的比率。实心度定义为面积除以该区域的凸包。实心度对于不规则形状很有帮助。凸包是可以在不与对象相交的情况下围绕对象拟合的最小曲线。它可以被视为尝试将弹性带拉伸到对象周围以完全包围它。对于椭圆或椭圆形状的对象,此参数不会添加任何信息。
让我们看看绘制几个椭圆和椭圆并测量它们的形状描述符时的一些结果。以下是椭圆选择(图像左侧)和椭圆选择(图像右侧)的一些示例:

圆度为 1.00 的形状被涂成橙色(椭圆)和浅绿色(椭圆)。红色椭圆的圆度非常低(0.28),而青色椭圆的圆度介于中等(0.48)。当尝试检测粒子时,此圆度参数将非常有用,因为它是一个使用单个值对形状进行基本描述的非常基本的描述。圆形物体将具有值为 1,而扁平的椭圆将具有接近 0 的低值。
您还可以使用椭圆工具创建一个环形选择。通过制作两个圆,一个比另一个大,然后移除较小的圆来实现。以下步骤将创建一个环形选择:
-
首先在您想要选择的物体上创建一个较大的圆圈,然后按 Ctrl + T 将其添加到 ROI 管理器中。将圆圈的大小设置为 20 x 20 像素。
-
要创建内圆,您可以使用椭圆工具添加一个新的圆。然而,它可能不会居中。要创建一个居中的内圆,我们将选择外圆,并从菜单中选择 编辑 | 选择 | 放大…。
-
输入
-5的值以将圆缩小到新的 10 x 10 像素圆,然后按 Ctrl + T 将其添加到 ROI 管理器中。 -
要创建环形,请在 ROI 管理器中选择两个圆。
-
在 ROI 管理器中选择两个圆,然后选择 更多 | XOR。这将生成环形。要添加新的选择,请按 Ctrl + T。
注意,以这种方式创建环形将迫使新的区域选择变为“像素化”。该区域的轮廓将与像素网格对齐,而不是 ImageJ 生成的 ROI。您也可以在创建内圆时按住 Alt 键来创建环形。然而,正确对齐两个圆可能很困难。
多边形选择
其他类型的区域选择是多边形选择和自由手选择。它们允许选择更多有机形状。要创建多边形选择,选择工具并通过左键单击,可以向多边形添加点。每个点通过一条直线(顶点)连接,通过双击或左键单击第一个点,多边形闭合并转换为区域选择。如果右键单击,多边形工具将在您单击的点处添加一个点,并同时闭合多边形。多边形至少需要三个点。这个工具有助于选择不规则形状,例如在块示例中:

对于这种不规则形状,多边形工具更适合仅选择块。使用自由手工具也可以达到类似的效果。然而,可能更难精确选择。自由手工具通过左键单击并按住按钮,在鼠标拖动形状周围时工作。
创建不规则区域选择的另一种方法是使用魔杖选择工具。这个工具在其他图形程序(如 Photoshop 和 Gimp)中的魔杖工具工作方式相同。它选择与单击像素具有相同强度或颜色的像素。要选择样本图像中的块,我们可以按照以下步骤操作:
-
从工具栏中选择魔杖工具。
-
左键单击一个块。这将创建一个选择。
-
通过双击魔杖工具按钮更改选择的公差,并将公差设置为
60。然后,按确定(见以下截图)。 -
左键单击相同的块以查看公差设置对选择的影响:
![多边形选择]()
魔杖工具允许我们设置公差,这意味着相对于所选像素落在公差范围内的值将被包括在选择中。0的值仅考虑与像素完全相同的像素。当启用阈值时,公差将被忽略(基本上,它被设置为0)。模式允许您使用 4-连接或 8-连接的邻居来确定选择。对于块示例,公差0(左图)和60(右图)之间的差异可能如下:

这个工具对于选择像这些块状有机形状非常有效。然而,它要求物体(块状)与背景之间的对比度要高。如果你将容差设置得更高,例如在块状示例中,不仅会选择物体,还会选择背景。如果你在块状示例中尝试150的容差值,选择仍然可以。然而,如果你点击一个块状较亮的像素,容差值为160时,几乎整个图像都会被包括在内。使用粒子分析器选择像块状这样的有机形状有另一种不同的方法,这将在稍后讨论。
线选择和测量
除了用于测量的区域选择工具之外,还有可用于测量的线选择工具。线选择对于选择细长结构非常有用。脑细胞具有一个基本的细胞体和被称为突起的长而细的进程。可以使用线选择来测量这些细长进程,以确定长度等特征。在时间序列中,沿着突起的线可以使用称为柱状图的专业动态可视化来测量强度随时间的变化。
柱状图
柱状图是每帧或切片沿线的所有像素的表示。这种类型的图像显示了物体的动态。从图像顶部到底部运行的直线代表静态物体,而斜线表示运动。角度越陡,物体移动越快。这可以用来测量物体的速度。它也是一种非常简单的视觉辅助工具,用于识别受限空间内的运动。这一点非常重要。任何开始在线上但离开线的一侧的物体将不会被可视化,也无法被测量。
让我们看看我们在上一章中当我们对时间序列进行归一化时使用的时间序列的非常基本的柱状图。在 ImageJ 中打开time_series.tif图像。接下来,我们将追踪一个存在许多小孔的拉伸区域。要追踪像我们这里这样的不规则形状,我们想要选择分割线区域。
-
在 ImageJ 主界面中右键单击线工具,并从提供的选项中选择分割线。
-
按照以下图像所示画线(可以自由选择不同的拉伸)。
-
如果你希望创建多个柱状图,你可以使用Ctrl + T将每条线添加到 ROI 管理器中。
![柱状图]()
确保你画的线与小孔相交,并且沿着它们的路径。如前所述,柱状图只会显示在线上的内容!作为一个练习,你可以通过首先创建最大投影并在那里画线,然后将它转移到时间序列上来更准确地画线。为此,请执行以下步骤:
-
通过转到图像 | 堆栈 | Z 投影…并选择最大强度来创建最大投影。
-
在新图像中,使用分割线选择一个区域并将其添加到 ROI 管理器中。
-
选择原始
time_series.tif窗口。 -
通过选择从 ROI 管理器中添加的 ROI 或通过转到编辑 | 选择 | 恢复选择从菜单来传输选择。请注意,后者选项仅适用于最后一个活动选择,而前者选项适用于任何数量的选择。
现在我们有了线条,我们将通过转到图像 | 堆栈 | 重新切片 [/]…从菜单中创建谱图。或者,你也可以按斜杠键(*/)。如果你有 Fiji 的最新版本,你也可以选择分析 | 多道谱图 | 多道谱图来创建谱图。当使用重新切片选项时,请按照以下步骤操作:
-
确保线条是活动选择,通过在 ROI 管理器中选择它来选择它。
-
从菜单中选择图像 | 堆栈 | 重新切片,或者按斜杠键。
-
选择避免插值复选框并按确定。
按下确定后,将打开一个新图像。它的宽度等于线条的长度,高度等于帧数(600)。以下是之前选择的前 300 行:

当使用 Fiji 时,使用位于分析菜单中的多道谱图工具可以生成相同的效果。此插件具有一个附加功能,允许你沿线条长度平均几个像素。这将通过平均像素强度来减少线条上的噪声效果。要使用此插件,请按照以下步骤操作:
-
在 ROI 管理器中选择线条选择。
-
从菜单中选择分析 | 多道谱图 | 多道谱图。
-
输入线条粗细的值:大于 1 的值将创建一个平均值,在这种情况下使用 3 并按确定。
谱图清楚地显示,一些点在移动,而其他点则更静态。根据你画的线条,你应该得到大致相同的结果,尽管某些区域运动较少。由于这个选择是单线,从这个例子也可以清楚地看出,一些点移动的方向与其他点不同。
要在波形图中测量速度(记住速度是距离除以时间),我们只需在每个非垂直部分绘制一条单线。垂直线的距离为零,因此速度为 0。为了使计算稍微容易一些,我们首先更改图像的校准(有关如何操作的详细信息,请参阅第二章, 使用 ImageJ 的基本图像处理)。将像素宽度设置为266.67,像素高度设置为0.125。单位可以设置为像素。我们在这里指定宽度(x 坐标)以纳米为单位,而高度(y 坐标)以秒为单位。ImageJ 不完全支持这个概念,但这对我们的目的仍然有效。
要执行测量,请按以下步骤操作:
-
选择直线工具,并在轨道中心绘制一条直线,直到直线保持在轨道中间。
-
确保在测量中选择了边界框选项。
-
按Ctrl + M键测量当前选择,这将添加到你的结果中。
-
通过拖动线的顶部手柄到下一个开始改变速度的点,并再次测量,来测量轨道的下一部分。
-
重复此过程,直到轨道从波形图中消失。
由于选择了边界框选项,你的结果表中将有四列,分别标记为BX、BY、宽度和高度。对于速度测量,我们只需要宽度和高度参数。宽度等于行进的距离,而高度等于行进的时间。为了得到正确的速度值,我们将宽度除以高度以得到速度,单位为nm/秒,并参考前面提到的校准。
注意
这种计算移动粒子速度的方法并不难,但非常耗时且容易出错。此外,波形图不适合在空间中任意方向移动的物体(即,不是沿直线移动)。因此,对于这种类型的物体和更详细的跟踪物体运动的方法,我们将在后面的章节中重新探讨这个话题。
线轮廓
在前面的章节中,我们看到了使用简单的线选择和时间序列进行量化可以做什么。线选择也可以用于单张图像,尤其是在量化与强度、(共)定位和完整性相关的特征时。对于这些类型的评估,我们想知道物体的强度分布。为了创建强度分布,我们可以使用直线或分段线选择。
在以下示例中,我们将查看树截面内环带的分布。树木生长时,每年都会增加一个年轮。环带越厚,树木生长越快,这表明生长条件有利(阳光、温和的温度、降雨、土壤条件等)。对于这种分析,我们需要知道两件事:环带的数量和每个环带的厚度。
首先,从文件 | 打开样本中打开树轮图像。打开的图像显示了一棵树的局部截面,树心大约位于位置(135,54)。环带可以看作是白色区域,由暗线分隔。每条暗线是生长环的边界。当你放大树的中心时,可以立即看出环带的宽度并不相等。例如,第四个环带非常薄,而第五个环带则厚五倍。为了分析,我们可以使用与波形图相同的方法。我们可以从暗边界到暗边界画一条线,并测量每次的长度。这有一个很大的缺点。它非常耗时(再次),而且很难保持线条的直线性。测量每个环带的宽度应该基于每个环带边界的最短距离。
要创建线形轮廓,请选择图像某部分中的一条线进行轮廓分析,然后从菜单中选择分析 | 绘制轮廓(或按Ctrl + K):

这显示了图像中线条底部的线形轮廓。它显示了沿线条像素的强度。绘制轮廓窗口还有一个选项可以显示实时更新的图表。这意味着当区域移动或调整大小时,图表会立即更新。轮廓中的每个低点都表示一个树轮边界。当我们按下列表按钮时,绘制轮廓窗口会打开一个窗口,显示沿线条每个像素的强度值。如果我们将其复制到电子表格程序中,我们可以计算每个山谷之间的距离,以确定每个环带的像素宽度(不校准图像,实际单位的真实宽度是未知的)。山谷的数量给出了年数。
或者,可以使用前面显示的轮廓图来测量环带的宽度。为此,我们必须在轮廓图上画一条分段线。ImageJ 中的每个图形窗口(不包括文本和结果窗口)都可以用来绘制选择。对于这个例子,在创建轮廓图之前,在轮廓图上禁用网格线很重要。这可以通过取消选中轮廓图设置中的绘制网格线复选框来完成,该设置可以通过从菜单中选择编辑 | 选项 | 轮廓图选项…来找到。
-
从左侧开始,从每个山谷的最低点画一条分段线。
-
使用颜色选择器将前景色设置为黑色,并通过Ctrl + D或通过菜单中的编辑 | 绘制来绘制线条。
-
确保您的测量设置包括边界矩形选项。
-
选择画笔工具来测量图表下方的面积。要测量树轮的宽度,使用画笔工具在您的分割线上方但低于图表的黑色线条处点击。这将选择图表的白色像素,这些像素连续到图表的黑色线条。您将看到,只有从您的分割线选择下方直到图表的线条下的部分被选中(见以下图片)。
-
当您按下Ctrl + M来测量此选择时,您将获得选择的宽度和高度。
![线形剖面]()
宽度测量将是树轮的厚度。通过在每个山谷之间进行此操作,您可以测量每个环的厚度。请注意,如果图表下方的区域对您的测量有意义,那么在您的测量选项中选择面积将也会提供此参数。在分析电泳凝胶和蛋白质印迹时,使用类似的方法。为此,ImageJ 有一个专门用于分析凝胶的工具集。这些工具可以通过转到分析 | 凝胶找到,有关如何使用它的说明请参阅imagej.nih.gov/ij/docs/guide/146-30.html#toc-Subsection-30.13,以及解释它的视频请参阅imagejdocu.tudor.lu/doku.php?id=video:analysis:gel_quantification_analysis。
共定位
在前面的章节中,我们探讨了测量图像某些方面的方法,例如速度和长度。本节将探讨涉及不同信号共定位的测量方面的不同。共定位意味着两个(或更多)物体彼此靠近。每当两个信号在空间上重叠时,我们可以得出结论,它们位于同一位置,在成像系统允许的分辨率范围内。在生物学中,两个标记的结构或蛋白质的定位提供了蛋白质是否包含在结构中或是否在受到刺激后移动到某个位置的线索。对于像细胞这样的动态结构,我们可以观察共定位量的变化,这取决于时间或刺激。
半定量共定位
半定量共定位意味着您通过肉眼或使用粗略的测量来检查共定位的量,并将其(相当任意地)分类为共定位或非共定位。这可以是一个非常好的起点。然而,如果结果不是黑白分明的,就很难得出任何结论。对于这种类型的共定位,我们只需要每个信号的图像,并且我们需要将它们合并以查看共定位。这有时在我们获取图像时自动完成,有时需要手动合并图像。要合并两个不同的图像,需要满足一些先决条件:
-
图像需要具有相同的大小(X,Y,可选的Z或T)
-
图像需要具有相同的类型(8 位、16 位等)
-
图像不能在各个通道的获取之间移动
如果满足这些条件,共定位的结果应该提供定性的结果。
要合并两个通道,请从菜单中选择图像 | 颜色 | 合并通道…。目前,ImageJ 支持使用七个不同的 LUT 将七个不同的图像合并成一个单通道图像:红色、绿色、蓝色、灰色、青色、品红色和黄色。最常用的组合是红/绿,共定位的结果显示为黄色像素。另一种很好的颜色组合是绿/品红,共定位显示为白色像素。后一种选项推荐用于出版物,因为色盲的人仍然可以欣赏到共定位。请注意,如果一个通道的强度非常低,而另一个通道的强度很高,人眼视觉系统将只能感知亮度更高的通道。为了良好地可视化共定位,两个通道需要相似的灰度值分布。相应的直方图应该看起来相似。
一种简单的方法是观察亮像素的重叠来粗略地量化这种共定位。为此,我们可以为每个通道设置一个阈值,并为每个图像创建一个掩码。要查看重叠,我们可以使用图像计算器执行AND操作。像素的重叠被分类为在两个图像中相同位置的像素值为 1(技术上为 255)。执行此AND操作后,我们可以通过计算结果图像中的白色像素数量来确定重叠量。一种简单的方法是通过按Ctrl + H来获取直方图,然后在结果直方图窗口中按列表按钮。通过查看列表底部的 255(列表底部)的值,您将得到重叠像素数量的计数。我们将在这个主题上重新讨论,在第八章,ImageJ 插件的解剖结构中,我们将使用 Fiji 提供的某些插件应用更严格的量化。
粒子分析
本节将探讨用于粒子分析的方法,这是一个处理在图像中检测多个(相似)对象,目的是对它们进行分割和计量的领域。许多问题都可以定义为粒子系统,它由单个图像中的许多单个细胞、表面上的孔洞、道路上检测汽车等组成。基本的粒子分析步骤是在单个图像中检测或分割粒子。
预处理和准备
要检测粒子,首先需要将它们从背景中分离出来。为此,我们需要创建一个蒙版,将所有对象从背景中隔离出来。我们已经在上一章中看到了如何设置阈值以及如何使用它来创建蒙版。这个蒙版图像将被用于粒子分析。在这个例子中,我们将使用一个相对简单的例子。通过在 ImageJ 菜单中选择文件 | 打开样本来打开Blob图像。当图像打开时,转到图像 | 调整 | 阈值…并使用自动按钮设置阈值。确保暗背景框没有被勾选。你现在应该看到以下类似图像:

红色区域表示前景,即我们潜在的粒子,而其他所有内容都将被忽略。现在的目标将是根据两个主要特征来分割粒子:它们的形状和大小。我们将通过从菜单中选择编辑 | 选择 | 创建蒙版来完成这一步骤(蒙版是右侧的图像)。
在我们开始检测粒子之前,我们首先需要了解一些关于它们的信息。我们需要知道粒子的尺寸。有两种简单的方法可以确定图像中所有粒子的尺寸,我们将从最直接的方法开始。为了确定特定粒子的尺寸,我们只需围绕它绘制一个区域并测量它。在面积测量的章节中,我们使用了多边形选择来测量面积和形状描述符。对于粒子分析,我们需要确定我们可能仍然认为它是真实粒子的最小粒子。
为了开始,让我们以(103,111)处的粒子作为最小的真实粒子。在它周围绘制多边形后,你可能得到一个面积为 363 像素,圆形度为 0.9188 的区域。如果我们取一个不太圆形的粒子,例如(133,83)处的粒子,我们得到一个面积为 434,圆形度为 0.7329。让我们取这两个观察结果中每个参数的最小值,得到一个最小面积为 363 像素,最小圆形度为 0.7329,然后继续。现在我们可以通过从菜单中选择分析 | 分析粒子…来执行粒子分析。在打开的对话框中输入以下参数:

尺寸是指将被计数的粒子的大小范围,以平方像素(面积)为单位。我们发现的圆形度范围现在定义为比我们的估计更圆或更圆的粒子。对于显示选项,您可以选择多个输出类型,包括轮廓。当您使用添加到管理器选项时,这种输出实际上不再需要,可以设置为无。当您不希望测量仅部分在图像中的粒子时,您必须选择在边缘排除选项。在第九章《创建 ImageJ 分析插件》中,我们将查看时间序列中粒子分析的一个实现。
点击确定后,粒子将被添加到 ROI 管理器中,此时粒子分割完成。现在我们可以使用与之前章节中查看的任何其他区域选择相同的方法来测量粒子。设置面积和圆形度参数的另一种方法是运行粒子分析,但不限制大小或圆形度参数。这将检测图像中的每个粒子,分割后可以过滤结果。这两种方法应该给您相似的结果,工作量相等。
摘要
在本章中,我们介绍了一些测量图像和时间序列中参数的方法。我们使用了之前章节中的一些技术来从我们的图像中提取数据。您学习了如何在单张图像中可视化动态数据(光栅图)。我们以定性的方式研究了共定位,作为本书后面定量分析的序言。最后,我们研究了粒子分析作为检测单张图像中相似对象的方法。
在下一章中,您将使用您所学的一些技术,并将它们应用到宏中以提高效率。
第六章:在 ImageJ 中开发宏
在本章中,我们将探讨自动化图像处理的方法,以便实现更快、更高效的图像处理。我们之前所做的处理是足够的,但耗时较长。当处理非常大的堆栈或时间序列,或者处理许多单个文件时,我们执行的处理是好的,但效率不高。我们将探讨 ImageJ 中的宏,并了解它们如何帮助我们进行图像处理。在本章中,我们将涵盖以下主题:
-
记录和运行宏
-
修改宏
-
宏中的用户输入
-
宏的进度
-
批量运行宏
-
安装宏
记录宏
宏是一系列命令的集合,允许您对单个图像或多个图像执行一系列任务。在宏中,您可以放置 ImageJ 菜单结构中可以找到的所有命令。宏的一个非常基本的应用是将图像从一种特定类型转换为另一种类型。为了创建一个宏,我们可以从头开始创建,在文本文件中键入所有命令,然后执行。然而,如果我们使用菜单结构中的命令,一个更简单的方法是使用宏记录器。
宏记录器将记录您所做的每个命令和选择,并将它们放置在一个简单的编辑器中。这是一种非常简单快捷的方法来创建一个宏,该宏将在图像上执行一组基本任务。要开始记录,从菜单中选择插件 | 宏 | 记录…,这将打开一个新的记录窗口:

记录窗口有一个列表,允许记录不同类型的记录。默认为宏,但也可以使用 Java 作为记录类型来记录插件命令。ImageJ 还支持 JavaScript 代码和 Beanshell 脚本以运行,这些类型也可以在此创建。当选择 JavaScript 或 Beanshell 时,记录的命令将与默认宏命令略有不同。还有一个选项可以设置您正在创建的新宏的名称。ImageJ 中的宏名称不需要下划线,并且具有.ijm扩展名,以表明它们是 ImageJ 宏。当您完成了所有希望应用于图像的处理步骤后,您可以按创建按钮来保存宏。
记录转换宏
让我们看看一个简单的宏录制,该宏将获取多通道图像,更改蓝色通道的查找表,并将其转换为 RGB 图像。我们将使用 HeLa 细胞的样本图像。为了更好的处理,我们不会包括打开图像命令。因此,我们首先通过转到文件 | 打开样本 | HeLa 细胞来打开图像。然后,我们将通过从 ImageJ 菜单转到插件 | 宏 | 记录…来开始宏记录器。确保类型设置为宏,并为你的宏输入一个名称。接下来,我们将按照我们希望使用的顺序执行我们希望记录的步骤。首先,激活图像窗口,并通过按两次右箭头键选择蓝色通道。你会看到现在记录器窗口中有两个命令:
run("Next Slice [>]");
run("Next Slice [>]");
注意,当你用鼠标点击通道栏时,不会记录任何内容,也不会向记录器窗口添加任何命令。
注意
记录器不会记录改变显示状态的鼠标点击。它不会记录用于更改通道、帧或堆栈切片的鼠标点击,也不会记录调整亮度/对比度的操作。只有设置和应用命令会显示在记录器中。
当选择蓝色通道时,我们将通过从菜单中选择图像 | 查找表 | 青色来将此通道的 LUT 更改为青色。这将在记录器中添加一个新命令,对应于我们刚刚执行的操作:
run("Cyan");
我们现在将执行此过程的最后一步,即将图像转换为 RGB 图像。要做到这一点,请从菜单中选择图像 | 类型 | RGB 颜色。将创建一个新的 RGB 类型图像,并在记录器窗口中添加一个新命令:
run("RGB Color");
现在,我们有一个完整的宏,可以将三通道图像转换为 RGB 图像,并改变一个通道的 LUT。最终的记录窗口将类似于以下截图:

我选择的宏名称是 convert_3ch_rgb.ijm。当你创建宏时,这将是你保存宏时使用的默认名称。当你点击创建按钮时,将打开一个新窗口,其外观会根据你使用的 ImageJ 版本有所不同。当使用 Fiji 时,将打开脚本编辑器窗口,其中包含我们在编辑器中记录的命令:

Fiji 脚本编辑器的优点是它具有语法高亮(由不同元素的颜色表示)和行号。它还支持标签式界面,允许在同一窗口中同时打开多个宏。此编辑器在窗口底部还有一个运行按钮,可以直接运行宏。
在标准的 ImageJ 发行版中,编辑器看起来会更基础一些,并且它没有 Fiji 脚本编辑器提供的附加功能:

注意编辑器中缺少高亮显示和行号。一旦我们创建了宏,我们可以使用键盘快捷键Ctrl + R或通过转到宏 | 运行(标准 ImageJ)或运行 | 运行(Fiji)来运行它。
宏的录制允许按顺序记录多个步骤。然而,我们现在所拥有的宏存在一个缺点:我们需要自己打开我们想要处理的图像,并且我们还需要保存处理后的图像。此外,当前形式的宏只处理当前活动的图像。只要只有一个图像打开,这不会造成问题。然而,当我们运行宏时打开多个图像,我们必须确保在运行宏之前选择我们想要处理的窗口。在下一节中,我们将尝试添加一些命令来处理打开和关闭图像,以便进行更稳健的处理。
修改宏
我们在上一节中创建的宏是有效的。然而,它没有处理打开和关闭图像。因此,现在,我们将查看修改我们现在拥有的当前宏的过程。大部分工作将在编辑器窗口中完成,但我们仍然会使用录制窗口来发现打开和关闭图像所需的函数。
让我们从给我们的宏添加一个选项开始,以便打开你想要处理的图像。我将使用我们之前使用的图像。然而,由于你更有可能在磁盘上本地保存图像,我们将把HeLa 细胞图像保存到本地磁盘上。让我们在桌面上创建一个名为processing的文件夹,并将HeLa 细胞图像保存在其中。要保存图像,转到文件 | 保存或按Ctrl + S,然后选择桌面上的文件夹作为目标。保存图像后,我们可以在宏中开始打开图像的过程。
首先,我们需要确保我们的上一个宏在编辑器窗口中打开,并开始宏录制器。我们还需要确保没有打开任何图像。现在,我们将通过选择文件 | 打开…来打开我们保存的图像。然后,我们将从之前步骤中创建的文件夹中选择图像。在录制窗口中,我们现在将看到一行告诉 ImageJ 打开图像。在我们的计算机上完成时,<username>将设置为登录时使用的用户名:
open("/Users/<username>/Desktop/processing/hela-cells.tif");
这告诉我们 ImageJ 需要带有单个参数的open()函数,这个参数是一行文本(在 Java/ImageJ 中称为“字符串”,由双引号分隔)。这一行文本包含您希望打开的文件的完整路径。为了将此命令实现到我们的宏中,我们需要将此命令复制或输入到编辑窗口的第一行。现在我们可以通过运行宏来测试一切是否按预期工作。为此,我们需要关闭图像并运行宏,通过选择编辑窗口并按Ctrl + R来运行宏。如果一切顺利,图像将打开,蓝色通道将被选中并变为青色,最后,图像将被转换为 RGB 图像。
接下来,我们将探讨如何将新图像保存到同一文件夹,但使用不同的名称。我们需要确保记录器窗口仍然打开,然后点击新创建的图像以激活它。接下来,我们将通过访问文件 | 保存将图像保存为 TIFF 文件。我们将保留 ImageJ 设置的名称。在记录器窗口中,我们现在应该看到一条新行,包含保存命令:
run("Save", "save=[/Users/<username>/Desktop/processing/hela-cells.tif (RGB).tif]");
此命令比打开命令复杂一些,因为它使用了更通用的run()方法。run()方法接受两个参数:一个包含命令的字符串(在这种情况下为保存)和一个形式为save=[]的字符串,其中方括号内为保存的文件名。请注意,在此命令中用户名被替换为<username>。它应该更改为您登录账户的用户名。现在我们可以将此命令添加到我们的脚本中,以执行保存功能。
接下来,为了完成这个过程,我们将关闭当前所有打开的图像。为此,我们将选择最近保存的图像并关闭它。或者,我们可以从菜单中选择文件 | 关闭所有。如果我们关闭一个图像或使用关闭所有,记录器窗口中分别将放置以下行:
close();
run("Close All");
第一行表示当前激活的窗口将被关闭,而第二个命令将关闭所有打开的图像。由于我们希望在处理完毕后关闭所有打开的图像,因此第二个命令更适合我们的宏。我们将添加“关闭所有”命令到脚本中,这意味着我们的最终宏将如下所示:

当在 Fiji 中使用脚本编辑器时,我们还可以在编辑器下方的列表中看到使用当前宏执行的操作。使用清除按钮可以重置此历史记录。当你以当前形式打开此宏时,它将打开图像,更改第三通道的 LUT,将其转换为 RGB,保存结果图像,然后关闭所有图像。尽管这个宏非常简单,但它处理图像的速度比我们手动处理要快得多。这个宏只有一个问题:它只适用于特定位置的单个图像。如果我们想处理许多不同的图像,这个宏将不太实用。我们不得不手动为每个要处理的文件更改宏。因此,在下一节中,我们将添加在宏运行时允许用户选择文件的功能。
宏中的用户输入
我们之前的宏在处理特定图像方面非常高效,但如果宏要求用户指定要处理的文件,它将更高效。为此,我们需要添加一个方法来要求用户选择文件。唯一的问题是,我们无法使用记录器来获取此功能。我们需要找到一个要求用户输入文件位置的函数,这可以通过 ImageJ 中可用的内置宏函数来完成。在 ImageJ 网站上,有一个所有可访问宏函数的详尽列表,您可以在imagej.nih.gov/ij/developer/macro/functions.html找到。函数按字母顺序排序。
打开特定文件
我们想要的函数是一个文件打开对话框,提示用户定位图像文件。在这个页面上找到函数的最简单方法是使用浏览器中的查找功能搜索相关关键词。为了找到我们需要的函数,我们将在搜索框中使用搜索词“file open dialog”。当我们输入搜索词时,页面上会有多个出现,因此我们将查看每个出现的所有描述。在这种情况下,描述我们所需功能的函数是名为File.openDialog(title)的函数,描述说明它将显示一个文件打开对话框,返回用户选择的文件路径。我们现在将修改当前的宏,使用此函数允许我们更改我们选择的任何文件。我们将修改宏的第一行,变成以下两行:
fname = File.openDialog("Select 3 channel image");
open(fname);
第一行指示 ImageJ 显示一个标题为选择 3 通道图像的文件打开对话框,并将用户选择的路径存储在一个名为fname的变量中。在下一行,我们修改了open()命令,使用fname变量打开用户选择的图像。在这个例子中需要注意的一点是,没有指定变量类型。ImageJ 中的宏是弱类型,不需要事先指定类型。
将图像保存到文件夹
因此,我们现在使宏变得更加灵活。我们现在可以选中硬盘或附加存储上任何位置的任何文件。唯一的问题是图像仍然被保存到固定位置和固定名称。所以现在,我们必须更改处理保存图像的宏的部分。这个问题有多个可能的解决方案。我们可以将新图像保存到打开的图像所在的同一文件夹中,或者我们可以将其保存到我们收集所有处理过的图像的不同文件夹中。我们将从第一个选项开始:将其保存到打开的图像所在的同一文件夹中。
要获取我们选择的文件夹的名称,我们可以使用一个名为 File.directory() 的函数,它将给出使用文件打开对话框打开的最后一个文件的目录。这正是我们保存函数所需要的,所以让我们首先在我们的代码中添加这个函数。为此,我们将在 run("Save", …) 命令之前添加一行,并添加以下代码:
fdir = File.directory();
这将把最后一个打开的图像的路径存储在一个名为 fdir 的变量中。为了保存一个文件,我们需要路径以及新文件的文件名。在这个情况下,文件名只是创建的图像的标题,所以我们将使用一个函数通过在 fdir 行下面添加这一行来获取当前图像的标题:
newName = getTitle();
我们将新文件名的值存储在一个名为 newName 的变量中。我们现在已经准备好修改 save 函数以使用我们创建的两个变量。我们需要做的是合并 fdir 和 newName 变量。我们可以在 save 命令中这样做,所以我们将旧的 save 命令更改为以下行:
run("Save", "save=["+fidr+newName+".tif]");
我们已经用两个变量替换了方括号中指定的路径。我们必须在方括号之间添加一组引号来中断字符串,我们使用了 + 操作符来连接字符串。我们指定了要保存的文件的扩展名。由于图像的标题名称末尾不包含扩展名,我们需要添加它。或者,我们可以使用 saveAs 宏命令来实现相同的结果(添加扩展名不是必需的,因为我们将以 TIFF 文件保存图像):
saveAs("Tiff", fdir+newName);
注意
在这种情况下,RGB Color 命令创建了一个新的图像。当在新的图像上调用 Save 时,我们可以更改名称,它将以与 saveAs 命令相同的方式工作。如果你的函数没有创建新的图像,但你仍然想将结果作为单独的文件存储,请使用 saveAs 命令。否则,保存函数将用修改后的数据覆盖磁盘上的图像。
我们宏现在应该看起来像这样:

我们的宏现在更加灵活。我们可以选择任何文件进行处理,结果将存储在原始文件相同的文件夹中,但名称已更改。下一步是修改代码以控制哪个通道将被修改。
添加选择
我们现在的宏允许我们修改任何我们可以定位并保存结果在同一文件夹中的图像。在下一步中,我们将询问用户我们希望更改哪个频道。我们需要询问用户一个介于 1 到 3 之间的数字,这将是要更新的频道。有两种基本的方法来获取一个数字:我们可以使用一个文本字段,用户可以输入一个数字,或者我们可以展示一个数字列表,用户可以从中选择正确的数字。第一种方法非常简单,但也需要额外的检查。如果用户输入的值大于频道数量(或者根本不是数字),会怎样?一个稍微安全的方法是给用户一个有限的选项集,其中只有一个可以被选中。
我们将使用一组与创建对话框和向其中添加字段相关的函数。我们需要在宏的开始部分放置此代码,在我们调用下一个切片命令之前。我们将以下代码直接放置在打开命令之后:
Dialog.create("Select a channel");
Dialog.addChoice("Channel number:", newArray("1","2","3"));
Dialog.show();
第一行将创建一个标题为选择一个频道的对话框。接下来,我们在新创建的对话框中添加了一个选择列表,其中包含选项1、2和3作为字符串。最后,我们调用了show()方法来显示我们创建的对话框。
我们创建了一个对话框,让用户选择一个频道编号,但我们还没有使用这个选择。如果我们现在运行代码,结果将与我们对话框中做出的选择无关。因此,我们的下一步是检索用户选择并提取用户选择的数字。执行此操作的功能是getChoice(),它是对话框功能的一部分。我们将直接在显示命令之后添加它,如下所示:
chChoice = Dialog.getChoice();
此命令将把选定的选择存储在一个名为chChoice的变量中。然而,如果我们查看该函数的描述,这个函数返回一个字符串。这是一个问题,因为我们需要它是一个数字,以便选择正确的切片。在宏语言中有一个函数可以将字符串转换为整数。它被称为parseInt(),我们可以这样实现它:
sliceNumber = parseInt(chChoice);
sliceNumber变量现在包含用户的频道选择。接下来,我们将使用这个数字来选择我们图像中的正确切片。我们可以使用一个小循环结合我们的下一个切片命令。然而,有一个更快更简单的方法,使用一个内置的宏函数,称为setSlice()。为此,将带有run("Next Slice [>]")的两行替换为以下行:
setSlice(sliceNumber);
如果我们希望更改将要使用的查找表的颜色,我们可以使用相同的方法在我们的对话框中添加第二个选择列表。我们只需添加另一个 addChoice() 命令,但这次,提供几个 LUT(例如,青色、黄色、品红色等)的选择。getChoice() 函数按它们添加到对话框中的顺序检索每个选择列表的结果。如果您在通道号之后添加 LUT 选择,它将通过 getChoice() 的第二次调用检索。现在,我们的宏将如下所示(我已添加颜色选择):
fname = File.openDialog("Select 3 channel image");
open(fname);
Dialog.create("Select a channel");
Dialog.addChoice("Channel number:",newArray("1","2","3")));
Dialog.addChoice("Select color:", newArray("Cyan","Magenta","Yellow"));
Dialog.show();
chChoice =Dialog.getChoice();
clrChoice = Dialog.getChoice();
sliceNumber = parseInt(chChoice);
setSlice(sliceNumber);
run(clrChoice);
run("RGB Color");
fdir = File.directory();
newName = getTitle();
run("Save", "save=["+fdir+newName+".tif]");
run("Close All");

现在有一个名为 clrChoice 的新变量,它保存用户所做的颜色选择的值。如果您运行修改后的宏,您将必须选择要处理的图像,然后设置通道和 LUT 颜色。之后,图像将根据您设置的值进行处理。现在,宏相当灵活,允许对特定通道进行不同类型的特定颜色转换。我们现在只需要进行一项修改,使其更加健壮。我们需要检查用户选择的图像实际上是否有三个通道。
执行输入检查
要在所选图像中添加对切片数量的检查,我们需要一个简单的条件语句。if 语句将执行此检查。我们将在打开图像后、请求用户输入之前添加此条件语句。如果通道数少于三个,我们需要停止宏的执行并关闭我们打开的图像:
if(nSlices<3) {
run("Close All");
exit("Not enough channels in the image (min. is 3)!");
}
nSlices 函数是一个内置的宏函数,它返回当前图像的切片数。我们将检查切片数的值与所需的值进行比较。如果通道不足,我们将关闭所有图像并使用 exit() 函数终止宏。
注意
当使用 nSlices 函数时,请记住,ImageJ 通过乘以图像的切片数、帧数和通道数来计算此值。当处理(超)栈时,nSlices 函数不会返回您可能期望的值。例如,一个具有两个通道、五个切片和 51 帧的 5D 图像将返回 510(2551)的值。对于栈,您可以使用 Stack 方法。要计算通道数,您可以使用 Stack.getDimensions() 函数。
exit 函数有两种形式:一种不带参数,另一种带字符串参数。字符串参数将显示一条消息,说明为什么宏被终止。建议您使用后者,通过提供反馈让用户了解为什么宏没有执行任务。现在,我们的宏应该如下所示:

我们的宏现在很健壮,每次运行时都会以可预测的方式运行。如果我们的图像切片非常少,它将终止。如果它有更多的切片,它将正确运行。然而,我们将无法修改第三层以上的任何切片。因此,我们的最终修改将检查图像中的切片数量,并通过操作切片号码数组相应地调整我们的选择。
要更改选择列表的内容,我们首先需要创建一个比当前指定的数组稍长的数组。我们希望限制通道数量,因此我们将创建一个最大通道数为五个的数组。我们还将调整对通道数量的检查,以反映这种更改。我们将首先创建一个包含数字1到5的字符串数组,并修改条件语句:
chNumbers = newArray("1","2","3","4","5");
if(nSlices>chNumbers.length) {
run("Close All");
exit("The many channels in the image (max is"+chNumbers.length+")!");
}
这将存储通道号码在chNumbers数组中,条件数组现在将检查切片数量是否不大于该数组的长度。这种方法允许我们轻松地将额外的通道号码添加到数组中,而无需修改任何其他代码。
接下来,我们在对话框中的选择列表中添加了通道号码列表。然而,我们必须考虑到用户可以选择比五个通道更少的图像,因此我们需要更改添加到选择列表中的数组,以反映所选图像中存在的通道数量。为此,我们可以使用在数组上工作的trim函数。trim函数接受两个参数:第一个是数组,第二个是一个整数,指定需要从第一个元素开始返回的元素数量。我们可以使用nSlices函数来给出我们希望trim函数返回的元素数量:
Dialog.addChoice("Channel number:", Array.trim(chNumbers, nSlices));
如果我们现在在我们的HeLa Cells文件上运行修改后的宏,我们会看到通道号码的选择列表只包含1、2和3这三个值,这正是我们预期在这个图像中会看到的。如果我们打开一个具有五个通道的另一个图像,我们可以在列表中选择五个选项。你可以通过保存Neuron (1.6M 5 channels)样本图像来测试这一点。最终的宏现在看起来就像以下截图:

在使用宏处理下一步之前,我们将创建一个宏,该宏将对文件夹内包含的文件列表执行处理步骤。这个过程需要某种形式的进度,以便让用户知道正在发生什么,并给出处理将花费多长时间的提示。
在宏中显示进度
在前面的章节中,我们看到了我们可以使用一个(相对)简单的宏来处理单个图像文件。尽管这个宏非常灵活,但它仍然需要用户每次都单独选择每个文件并设置值。很多时候,你希望对一组许多相似的图像执行相同的处理步骤。这些图像具有相同的规格(通道数、颜色等),但属于不同的样本或个体。在处理大量图像时,显示进度以指示我们处理到了哪一步,并提供一些视觉反馈,表明已经处理了多少项目,是非常有用的。最简单的反馈类型是显示已处理的文件百分比。任何低于 100%的值都表示我们还没有完成。如果我们跟踪处理 10%的图像所需的时间,我们可以(大致)估计完成处理所需的时间。另一种有用的反馈类型是在处理结束时提供一条消息,表明我们已经完成。
对于本节,我们将创建一个不同的宏,该宏将接受一个包含时间序列图像的文件夹,每个文件夹包含 20 帧和两个通道。我们将取每个图像的第一个通道,创建前五帧的平均投影,并将结果保存在同一文件夹中。然后我们将取每个图像的第二个通道,创建最大投影,并将其保存在同一文件夹中。该文件夹将包含 10 个需要处理的文件,以及一个包含文件夹中文件描述的单个文本文件。我们将首先创建下一节中处理步骤的宏。
处理时间序列
我们将首先创建执行处理的步骤。我们可以使用记录器以及 ImageJ 网站上的内置宏函数参考页面来帮助我们处理。我们首先以常规方式在 ImageJ 中打开图像。从文件夹中逐个打开图像的代码将在稍后编写。一旦图像打开,我们将创建处理每个通道的代码。我将介绍一些有用的结构,使代码更加清晰。我将在代码中添加注释以指示正在发生的事情,并将处理封装在函数中。
让我们从创建一个生成绿色通道平均投影并保存它的函数开始。在 ImageJ 宏中创建一个函数非常简单。为了声明一个函数,我们将使用function关键字,后跟函数名称和参数列表。对于我们的绿色通道,函数声明如下:
function processGreenChannel() {
}
函数名为 processGreenChannel,它没有参数(稍后将会改变,但现在这样是可以的)。我们的第一个处理步骤是生成绿色通道的平均值。有好多方法可以做到这一点,但现在我们将使用最基本的方法。我们将为两个通道创建平均值,并在保存之前移除我们不需要的通道。为了创建平均投影,我们需要记录器来发现命令的格式。如果记录器还没有打开,请通过访问 插件 | 宏 | 记录… 来启动记录器。接下来,我们将从菜单中选择 图像 | 堆栈 | Z 投影...,并将 5 输入为 停止切片,将 平均强度 作为方法。我们会看到记录器中显示给我们结果的命令:
run("Z Project...", "stop=5 projection=[Average Intensity]");
这将是我们将添加到处理绿色通道函数中的第一个命令。接下来,我们希望从平均投影中移除红色通道。为此,我们将从菜单中选择 图像 | 堆栈 | 删除切片。将打开一个对话框,让我们选择删除通道(这里实际上没有选择)。通过按 确定,第一个通道(红色通道)将被移除。记录器显示我们使用的命令如下:
run("Delete Slice", "delete=channel");
我们可以将我们在上一个宏中用于保存图像的相同代码添加到这里。现在我们的函数将如下所示:
function processGreenChannel() {
//create an average projection of the first 5 frames
run("Z Project...","stop=5 projection=[Average Intensity]");
//delete the red channel
run("Delete Slice","delete=channel");
//save the new image
fdir = File.directory();
fname = getTitle();
run("Save","save=["+fdir+fname+"]");
//close the saved image
close();
}
注意,我们不需要在名称末尾添加扩展名。投影命令使用前缀来更改名称(AVG_),而我们的原始图像已经有一个末尾的扩展名,该扩展名被 projection 命令保留。处理绿色通道的最后一步是关闭我们创建并保存的图像。然而,这次我们不能使用 close all 命令,因为我们还没有完成原始图像的处理。我们只需使用 close 命令,该命令仅关闭由我们函数的最后一行指示的当前活动图像。我在函数中包含了一行注释,以指示下一行(几行)将要发生的事情,作为理解接下来会发生什么的辅助。这是一个非常基础的编程工具,在我们几周或几个月后检查代码时可以节省很多时间。单行注释由前面带有两个正斜杠的文本表示。如果您需要多行文本以提高可读性,可以使用多行注释,它们以 /* 开始,以 */ 结束:
//single-line comment
/*
multi line comment
that is spread over
several lines.
*/
我们将要为红色通道创建的函数非常相似,但我们将现在使用不同的投影方法。此外,我们必须删除与上一个函数不同的通道。处理红色通道的完整函数如下:
functionprocessRedChannel() {
//create the maximum projection
run("Z Project...","projection=[Max Intensity]");
//select the green channel (which is number 2)
setSlice(2);
//delete the green channel
run("Delete","delete=channel");
//save the new image
fdir =File.directory();
fname = getTitle();
run("Save","save=["+fdir+fname+"]");
//close the new image
close();
}
与处理绿色通道相比,只有两个小的改动。投影类型从平均强度更改为最大强度,并且在调用删除通道函数之前添加了setSlice命令来选择绿色通道。请注意,如果我们还想对每个通道进行测量,我们可以在close()语句之前添加一些测量代码或函数调用,以在所选通道上执行测量。
现在我们已经完成了每个通道的处理代码,我们可以看到这两个函数之间有很多相似之处。我们可以创建一个单一函数,使用几个输入参数来相应地处理每个通道。在这种情况下,你需要三个参数:一个用于投影的停止点,一个用于投影类型,以及一个用于要移除的切片号。我们可以对当前函数这样做,但可能更简单的是保持函数分开。如果我们想在绿色通道中更改某些内容,处理它可能意味着我们必须向函数中引入更多的参数来使其工作。这将使函数调用非常复杂。因此,保持两个单独的函数更容易。在这个上下文中,唯一有用的参数是保存图像的目录。因为我们将要编写处理整个目录的代码,所以我们将已经拥有那个文件夹的路径,因此我们可以轻松地将它作为参数添加。我们将按如下方式修改绿色通道处理函数的函数定义:
function processGreenChannel(fdir) {
...
}
这意味着我们可以移除(或注释掉)在函数体内为fdir提供值的行:
//fdir =File.directory();
在这种情况下,我选择注释掉该行而不是直接删除它。如果只涉及几行代码,这通常是一个好的做法,因为它展示了函数应该如何工作以及变量的功能是什么。然而,对于大段代码,不建议这样做,因为代码会变得非常长,而且需要跳过的死代码也会很多。
我们接下来的步骤是选择文件夹,以创建需要处理的文件列表。为了概述,我们也将为此创建一个函数。这个函数的第一步是要求用户输入包含需要处理的文件的文件夹。当我们搜索参考网页时,我们会找到一个名为getDirectory(string)的函数,它提供了我们需要的功能。在描述中,还有一个对getFileList函数的引用。这个函数将返回指定目录路径中的文件列表。我们需要这两个函数来处理文件夹,该函数看起来如下:
function processFolder() {
//get a folder for processing
fdir = getDirectory();
//create a list of files that we need to process
flist = getFileList(fdir);
}
我们添加这个函数描述的位置对我们宏中的处理并不重要。声明可以在宏内的任何位置,但我会将其放置在代码的开头。按照你期望它们被调用的顺序放置函数声明是有意义的。
在这一点上,介绍一个可用于调试 ImageJ 宏的简单工具可能是有用的:日志窗口。日志窗口是一个文本窗口,可以打印变量的值,让你看到该值是否是你期望的。它还可以用作用户参考,以查看哪些文件夹已被处理,从而避免一个文件夹被处理多次。我们将在我们的函数中添加一个日志调用,显示正在处理的文件夹以及该文件夹中存在的文件数量。将以下行直接放在flist语句下面将产生以下输出:
//display the folder and the number of files
IJ.log("Current folder: "+fdir);
IJ.log("Nr of files: "+flist.length);
最后一步是遍历每个文件并在我们打开的图像上运行我们的处理函数。为此,我们将使用一个基本的循环结构,即for循环:
//go over all the files in the file list
for(i=0; i<flist.length; i++) {
//get the full file name
fname = fdir + flist[i];
//open the image specified by fname
open(fname);
//process each channel
processGreenChannel(fdir);
processRedChannel(fdir);
//close all images when we are done
run("Close All");
}
我们使用Close All语句结束循环,以确保在继续处理下一个文件之前所有图像都已关闭。
注意
Fiji 还提供了一小部分模板,这些模板允许在宏中为图像处理提供一个通用框架。对于 Fiji 来说,有两个模板非常有用。第一个是Process Folder模板(Templates | IJ1 Macro | Process Folder),它可以用于与我在这例子中使用相同的目的。另一个模板是Scale All ROIs模板(Templates | IJ1 Macro | Scale All ROIs)。这个模板告诉我们如何在 ROI 管理器中遍历一系列 ROI 并改变 ROI 的大小。
对于这个循环,我们可能还想显示处理进度,以指示我们已经处理了多少文件。为此,我们将添加对showProgress()函数的调用,该函数接受一个介于0和1之间的单个参数,表示已处理的文件比例。我们可以在close all命令之后直接放置这个调用:
//show the progress
showProgress((i+1)/flist.length);
由于 Java 中的数组是从零开始的,我们在索引中添加了一个1的值来指示已处理的文件编号。进度条将在 ImageJ 窗口的右下角显示。这完成了处理整个文件夹的宏,但在当前状态下,当我们运行它时,我们仍然会遇到两个问题。我们只有函数定义,但没有直接调用这些函数。我们缺少宏的入口点。这个问题很容易通过在宏的开头添加对processFolder函数的调用来解决。
第二个问题解决起来稍微困难一些。如本节开头所述,我们希望在处理的文件夹中也有一个文本文件。如果我们现在运行代码,这个文本文件也会被我们的宏打开。这将导致我们在尝试使用我们的函数处理通道时出现错误。如果我们的文本文件是最后处理的文件,这不会是一个大问题(只是有点马虎)。然而,当我们的文本文件在开头或中间某个位置时,宏将在一个不确定的点终止,我们不得不手动纠正它。这将抵消使用宏处理文件夹的全部好处。这将导致我们仍然需要手动检查每个文件。
我们可以通过从我们的文件夹中删除文本文件来解决这个问题,如果只有一个文件夹,这可能是一个不错的解决方案。然而,如果您有多个文件夹需要处理,这种方法可能不太有用。此外,删除文本文件意味着您将丢失其中包含的信息,这可能是重要的。另一个选择是在您的处理文件夹中创建一个子文件夹并将文本文件放在那里。这个解决方案也存在问题。文件夹也被 Java 视为文件类型。在创建文件列表时,子文件夹仍然会被包括在内。尝试使用打开命令打开子文件夹可能会产生意外的副作用。
我们可以通过在循环中添加一个条件语句来检查我们正在处理的文件类型来解决所有这些问题。这个if语句需要检查两个条件:当前文件是否是目录以及它是否是图像。为此,我们将在打开和处理命令周围添加以下if语句:
//verify that this file is correct
if(!File.isDirectory(fname) && endsWith(flist[i], ".tif")) {
...
}
这个if语句检查存储在fname变量中的完整路径是否不是目录,以及当前文件名是否以.tif结尾。这个检查将排除任何目录以及任何没有.tif扩展名的文件。showProgress调用可以保持在if语句之外。完成的宏可以从 Packt Publishing 的网站下载以供比较(batch_project.ijm)。当我们运行宏时,我们会看到处理过程相当迅速,并且在处理过程中主 ImageJ 窗口中会显示进度条。根据图像数量和您计算机的处理能力,处理可能太快而无法看到所有内容。
我们可以向当前的宏添加一个参数。这个参数可能会加快处理速度,并在处理时防止显示所有图像。这可以通过以下命令来控制:
setBatchMode(true);
当批处理模式设置为true时,图片将不会显示,只有新创建的图片将可见。如果值设置为false,则图片将显示。通过将批处理模式设置为true,在某些情况下可以实现 20 倍的速度提升。在下一节中,我们将探讨使用 ImageJ 内置方法运行宏的另一种方式:批处理模式。
批处理模式下运行宏
在前面的章节中,我们探讨了使用具有不同处理功能的宏处理文件夹。前面描述的方法非常灵活且强大,允许对处理流程和要处理的内容有很高的控制。然而,ImageJ 还有一个可以执行类似任务的方法,即批处理命令。此命令允许您在文件夹上运行您创建的指定宏,并允许您将结果存储在同一个文件夹或不同的文件夹中。要启动批处理命令,请转到 ImageJ 菜单中的处理 | 批处理 | 宏…,这将打开以下对话框:

您可以使用按钮设置输入和输出文件夹。您还可以设置输出格式。如果您没有设置输出文件夹,图片将不会保存,除非您在自己的代码中保存它。您可以使用 ImageJ 附带的一个宏,通过添加宏代码选择器,或者您可以使用打开…按钮来加载自己的代码文件。为了指定您只想处理图像文件,您可以使用文件名包含字段来指定一个模式,通过输入(.tif)并包含括号来表示您只想处理 TIFF 文件。当您按下处理按钮时,文本字段中显示的代码将为每个匹配该模式的图片运行。
注意,您的宏需要遵循一些规则才能在批处理模式下使用。如果您希望在宏内部自行执行保存操作,您需要在宏中放置保存结果的代码,并在批处理对话框的输出字段中留空。为了执行我们之前宏中执行的任务,我们将整个代码复制到批处理对话框中。然后,我们将删除processFolder()函数及其调用,用一行代码替换它,该代码提供打开图像的当前目录,然后调用处理函数:
fdir = File.directory();
processGreenChannel(fdir);
processRedChannel(fdir);
//processing functions...
我们可以在处理对话框中留空输出字段,因为图片是在我们的处理函数中保存的。我们可以在文件名包含字段中添加(.tif)以确保文本文件被跳过。当我们点击处理按钮时,文件夹将以类似的方式进行处理,并将结果存储在我们宏的处理函数中描述的方式。
这两种方法都非常适合处理整个文件夹,并且结果相似。最大的区别在于批量处理模式对处理步骤的控制略少,并且不允许递归处理文件夹和子文件夹。此外,在处理文件夹之前,无法包含多个用户输入或对话框。批量处理命令中的代码需要自给自足。任何用户输入都将在每次迭代中输入。
安装宏
一旦你创建了你的宏,你可以在 ImageJ 文件夹中的宏文件夹中保存它。当你想运行你的宏时,你可以转到插件 | 宏 | 打开或插件 | 宏 | 运行来打开和运行你的宏。你还可以将你的宏添加到宏菜单中。你可以通过从菜单中选择插件 | 宏 | 安装…来在 ImageJ 中安装一个宏。一旦你选择了你的宏,它将被添加到宏菜单的底部。你还可以将你的宏添加到macros文件夹中的StartupMacros.txt文件中。在此文件中提到的所有宏都将自动添加到宏菜单中。
注意
注意,在 Fiji 中,当你使用安装选项时,宏仅在该会话期间添加。一旦重启 Fiji,宏菜单将重置为默认内容。因此,建议你始终将你的宏和脚本放在 Fiji 的宏或脚本文件夹中。要始终在 Fiji 运行时加载它,请使用以下代码中描述的StartupMacros.fiji.ijm文件。
如果你想创建一个包含你经常使用的宏的按钮栏,你可以通过修改StartupMacros.txt文件(或 Fiji 的StartupMacros.fiji.ijm文件)来实现。如果你经常使用多个宏,这将非常有用。要将你的宏作为工具栏菜单添加,请在启动文件中的某个位置添加以下结构:
var myTools = newMenu("My awesome tools",
newArray("Macro_1", "Macro_2", "-", "Macro_3"));
macro"My awesome tools - C037T0b11MT7b09aTcb09t" {
cmd = getArgument();
if(cmd== "Macro_1")
runMacro("/PATH/TO/Macro_1_tool");
else if(cmd == "Macro_2)"
runMacro("/PATH/TO/some_other_tool");
}
newMenu方法的第一个参数是菜单项的名称;在这种情况下,我使用了My awesome tools。第二个参数向菜单添加一个宏命令数组,当添加到菜单中时,这些命令将在工具栏菜单中显示。如果在数组中添加破折号,则在该位置在菜单中添加水平分隔线。这可以用来将具有相似功能的宏分组。在定义菜单后,我们可以使用if...else结构实现菜单项,通过使用getArgument方法比较所选命令,以确定需要启动哪个工具。如果我们想知道运行宏所需的命令,我们可以启动宏记录器,然后转到插件 | 宏 | 运行…,选择我们的宏,并查看我们的宏命令是什么。
也可能给我们的菜单添加一个图标,这个图标需要在我们的宏实现后面指定为一个字符串。这个字符串由一系列指令组成,用于绘制我们用字母和坐标指定的元素。例如,如果我们想写出字符串Mat(我的神奇工具),我们可以使用以下字符串作为图标:

下划线的字符是我们希望添加的字母,而它前面的值是字体大小(分别为11、09和09)。字母T表示必须绘制字符,而它旁边的值表示字符的位置。还可以使用以下格式绘制多边形(这需要 ImageJ 1.48k):
Gxyxy...xy00
绘制这个图标可能有些复杂,在 Fiji 中有一个Beanshell脚本可以将图像转换为工具栏图标字符串。这可以通过打开一个图像并转到插件 | 示例 | 图像到工具图标来实现。还有一个提供更多灵活性和更高质量按钮的替代方案,这个替代方案是ActionBar,由Jerome Mutterer开发,这是一个创建可以按个人喜好设置的独立工具栏的插件。它还支持 PNG 格式的图标。
注意
ActionBar 的文档可以在imagejdocu.tudor.lu/doku.php?id=plugin:utilities:action_bar:start找到。它还包含了一个如何创建自己的工具栏以及如何在启动 ImageJ 时自动加载工具栏的示例。
摘要
在本章中,你学习了如何使用记录器创建宏,以发现我们可以应用的操作和函数。我们创建了一个基本的宏,该宏处理图像并生成新的图像。接下来,我们查看处理一个图像文件夹,并将生成的图像保存到磁盘上。最后,我们查看批处理模式,该模式允许 ImageJ 以类似的方式处理文件夹。在下一章中,我们将更详细地探讨可用于开发插件的构造以及如何设置插件开发环境。
第七章. ImageJ 结构的解释
在前一章中,我们开发了宏以简化我们的处理和测量。我们使用了 ImageJ 宏语言特有的技术和结构。在本章中,我们将探讨以下主题,为开发我们自己的插件做准备:
-
宏和插件的框架
-
ImageJ 中的特殊类
-
宏的内建函数
-
API 函数
-
设置 NetBeans IDE 进行开发
-
使用 Maven 进行开发设置
宏和插件的框架
我们将查看 ImageJ 为开发者提供的处理图像及其处理的一些工具。在前一章中,我们查看宏以执行常见的图像处理步骤。这已经比逐帧处理时间序列有了改进,但 ImageJ 支持更多工具和结构,允许您进一步扩展这些基本工具。在本章中,我们将查看一些这些结构,为即将到来的章节做准备,在这些章节中,我们将查看插件及其实现。
ImageJ 有两种更自动化的处理方式:宏和插件。除了前一章中描述的宏之外,ImageJ 还支持其他基于 Java 的脚本语言,如 Beanshell 和 JavaScript,以及其他如 Python 和 Ruby 等脚本语言。插件也可以分为两组:基于原始 ImageJ 的插件(即 ImageJ1.x 插件)和基于 ImageJ 下一个发展阶段ImageJ2(ImageJ2 插件)的插件。ImageJ2 的开发旨在与 ImageJ1.x 向后兼容,尽管这可能在将来发生变化。在本章中,我们将探讨在创建脚本和插件时可用的一些结构。我们将首先查看 ImageJ 支持的脚本语言。
宏和脚本语言
正如我们在前一章中看到的,我们可以通过启动宏记录器并执行不同的图像处理和测量步骤来轻松创建一个 ImageJ 宏。我们在记录器中将类型设置为宏。我们还可以为 ImageJ 支持的另外两种脚本语言做同样的事情:Beanshell和JavaScript。Beanshell 脚本是一种宏类型,但可以访问完整的 ImageJ 和 Java API。这意味着,除了宏中可用的命令之外,您还可以使用 Java 中的类和接口,这为处理提供了更多的选项。Beanshell 脚本语言的优势在于它是一种解释型语言(在运行之前不需要编译)并且只需要一个占用空间小的解释器。这使得创建快速解决方案和插件原型变得容易。在接下来的章节中,我将检查 Beanshell 脚本语言中的一些概念。请注意,在 ImageJ 的 JavaScript 语言中也可以实现类似的结果,唯一的区别是语法上的微小变化。
Beanshell 脚本
BeanShell 脚本允许你创建一个具有宏所有优点的脚本,同时还能访问 Java API。你可以几乎直接使用 Java 代码。然而,也有一些细微的差别。BeanShell 脚本具有弱类型。这意味着,你不需要声明变量类型,并且可以在运行时更改变量的类型。在其他所有方面,它与开发 Java 代码相当。如果你希望使用 Java API 中的类或接口,你需要首先使用以下代码行导入它:
import java.awt.event.KeyListener;
这个导入语句告诉脚本解释器它需要从 java.awt.event 包中加载 KeyListener 类。这将允许你监控按键。KeyListener 类是一个接口,可以附加到脚本的一个实例上。当按键时,该类将生成一个事件,导致调用 keyPressed() 方法,该方法必须由脚本重写。使用 keyPressed() 方法,你可以在按下特定键时执行特定任务。
BeanShell 脚本语言还支持将现有脚本导入到新脚本中。这样,你可以将多个脚本串联起来。串联脚本意味着你使用一个脚本的输出作为下一个脚本的输入,依此类推。这种处理方式的优势在于,每个脚本都成为一个可以重用并以不同方式组合以实现不同结果的模块。要将现有脚本导入到你的脚本中,请使用以下语法:
this.interpreter.source("some_script.bsh");
这将加载名为 some_script.bsh 的 BeanShell 脚本,并为你提供访问其方法的能力。一个简单的 BeanShell 脚本可能包含一系列基本的 ImageJ 命令,但也可以包含类、函数,甚至图形用户界面。现在,我们将探讨在 BeanShell 脚本语言中用于处理 ImageJ、图像和选择的几个结构。
ImageJ 主类
要访问主 ImageJ 窗口,我们可以使用 IJ 类来获取 ImageJ 的当前实例。我们可以使用这个实例来访问 ImageJ 类提供的一些参数:
protected ImageJ ij;
ij = IJ.getInstance();
Label status = ij.statusLine;
status.setText("Now we modified the status line text!");
在这个简短的示例中,我们创建了一个 ImageJ 类型的变量,并将 ImageJ 窗口的当前实例的引用存储在这个变量中,称为 ij。接下来,我们提取了 ImageJ 状态行的内容,并将其存储在一个名为 status 的变量中。最后,我们将状态行的文本设置为 Now we modified the status line text!。当然,这个示例既不直接有用也不完整,但它展示了如何获取主 ImageJ 接口并修改接口的一个组件。
注意
我使用了两种不同的声明和实例化变量的方式:ij 变量首先声明然后实例化,而 status 变量则在同一行中声明和实例化。如果变量需要扩展的作用域(即,跨越一个循环或整个类),则前者是必需的。
您也可以使用IJ类通过run()方法或如openImage()等方法来执行 ImageJ 菜单结构中的命令:
import ij.IJ;
IJ.run("In [+]", "");
imp = IJ.openImage("http://imagej.nih.gov/ij/images/blobs.gif");
第二行显示了如何使用IJ类的run()方法进行一次缩放。第三行显示了如何使用openImage()方法打开一个图像,在这个例子中,它将图像的引用存储在名为imp的变量中。要从 BeanShell 脚本内部访问图像,我们可以使用前面描述的openImage()来打开图像。或者,我们也可以使用当前的活动图像(如果已打开图像):
imp = IJ.getImage();
注意
注意,在 Fiji 中,它使用 ImageJ2 候选版本,在使用IJ类的方法之前,需要添加导入语句。在 ImageJ1.x 中,这些包是自动加载的,导入语句是可选的。为了确保您的脚本具有未来性,包含导入语句是最佳实践。
图像处理函数
使用 ImageJ 类,您可以访问当前的活动图像以及打开图像的方法。还有允许您使用ImageProcessor类在像素级别处理图像的方法。此类提供了可以在像素级别修改图像的方法:单个像素或一组像素。以下代码片段显示了如何使用ImageProcessor类更改特定像素的值:
import ij.IJ;
import ij.process.ImageProcessor;
imp = IJ.openImage("http://imagej.nih.gov/ij/images/blobs.gif");
ip = imp.getProcessor();
ip.invertLut();
imp.setProcessor(ip);
ip.putPixel(64,128, 255);
在这个例子中,我们打开了Blobs样本图像并获得了ImageProcessor。然后我们反转了 LUT(当打开时Blobs图像使用反转的 LUT)并将反转的图像放回imp对象中。最后,我们将坐标(x = 64,y = 128)处的像素值设置为255。在这个例子中,您将在一个位于(64,128)位置的 blob 中看到一个白色像素。
如果活动图像是 8 位图像(例如Blobs图像),这将导致一个白色像素。在 16 位图像中,此操作将导致一个深灰色像素。如果您想了解当前图像是否是灰度图像以及它有多少位每像素(8、16、24 或 32),您可以包含以下命令:
bGray = ip.isGrayscale();
bitDepth = ip.getBitDepth();
这将允许您确切地确定您正在处理什么类型的图像。如果bGray为true,则图像是 8、16 或 32 位浮点灰度图像或具有红色、绿色和蓝色通道像素值相同的 24 位图像。bitDepth值将告诉您它是哪个级别。这种区别很小,但很重要。包含颜色信息的 24 位图像与灰度的 24 位图像不同。后者可以在不丢失信息的情况下转换为 8 位图像,而前者则不能在不丢失颜色信息的情况下转换为 8 位图像。
选择函数
要访问 ROI 管理器中的选择,BeanShell 脚本允许您获取 ROI 管理器的实例,然后可以使用它提取 ROI 并用于处理。以下代码片段从 ROI 管理器中获取 ROI 并将它们放大2像素:
import ij.IJ;
import ij.plugin.frame.RoiManager;
imp = IJ.getImage();
RoiManager rm = RoiManager.getInstance();
int numRois = rm.getCount();
for(i=0;i<numRois;i++) {
rm.select(i);
IJ.run(imp, "Enlarge...", "enlarge=2");
rm.addRoi(imp.getRoi());
}
此代码片段展示了处理感兴趣区域(ROI)的一些基本脚本。我们首先检索 ROI 管理器的实例,这允许我们将 ROI 作为数组进行处理。在循环中,我们选择每个 ROI 并使用放大命令在X和Y方向上增加 ROI 的大小2像素。最后,我们将放大的 ROI 添加到 ROI 管理器中,以便我们稍后可以使用它们。此代码几乎可以原样用作 Java 代码。如果您尝试编译它,您会收到的第一个错误包含在以下行中:
imp = IJ.getImage();
这在 BeanShell 脚本中运行得非常好,但在 ImageJ 插件中,它将生成一个错误,因为imp变量的类型未声明。此外,for 循环没有为索引i迭代器声明类型,这也会生成编译器错误。
保存和运行您的脚本
一旦我们创建了可测试的版本,我们就可以保存它并尝试运行。宏以.ijm或.txt扩展名存储。.ijm扩展名更可取,因为它允许区分常规(非脚本)文本文件和宏文件。脚本文件有自己的扩展名:BeanShell 的.bsh和 JavaScript 的.js。
保存脚本时,命名必须遵守文件系统的限制。否则,对名称没有具体限制。默认情况下,存储脚本的目录是 ImageJ 安装文件夹中的scripts或macros文件夹,我将称之为$IJ_HOME。如果您想从命令行运行脚本,最好避免在文件名中使用空格,以避免出现意外行为(即,如果您忘记转义空格字符)。
注意
对于 Fiji,$IJ_HOME文件夹被称为Fiji.app,可以放置在文件系统的任何位置。建议您将此文件夹存储在您的用户账户文件夹中,您有读写权限。在 OSX 系统上,使用包安装器时,Fiji 的默认位置是/Applications/Fiji.app。
如前一章所述,关于 ImageJ 宏的描述,BeanShell 和 JavaScript 脚本可以以类似的方式安装和执行。当使用 Fiji 时,您可以在代码编辑器中打开脚本以运行它们。
ImageJ 插件
如我们在前几节关于 BeanShell 脚本语言的讨论中看到的,ImageJ 提供了一个易于访问的接口,可以访问完整的 Java API。这一点同样适用于插件。除了核心的 ImageJ API 之外,插件还可以通过在其源文件中导入类或接口来访问完整的 Java API。随着 ImageJ 社区的发展,正在开发一个新的 ImageJ 核心代码实现,称为 ImageJ2。在接下来的几节中,我将简要概述一些将影响插件开发的变更。这包括引入一些在大型项目中常用到的结构,特别是 Git 和 Maven。请注意,了解这些结构对于创建插件不是必需的,但它们将有助于创建更一致和可重复的代码。这些结构也不是 ImageJ2 特有的,但 ImageJ2 项目是基于这些概念构建的。然而,我将首先介绍一些特定于 ImageJ 的类,它们用于处理图像和选择。
ImageJ 主类
主要的 ImageJ 类指的是提供访问 ImageJ 应用程序的类。我们之前在 BeanShell 部分已经看到了这个类。这个类被称为 IJ,它是一个静态实用类。如前所述,这个类允许访问当前图像以及其他功能。这个类的使用方法与之前相同,只是在编写插件时,需要显式声明变量类型。例如,当我们希望创建一个包含两个 16 位通道、10 帧和 512 X 512 像素大小的新的超堆栈时,我们可以使用以下代码片段:
import ij.IJ;
import ij.ImagePlus;
ImagePlus imp = IJ.createHyperStack("New Stack",512,512,2,1,10,16);
注意,我们需要指定imp变量是ImagePlus类型,这与我们之前看到的脚本语言不同。IJ类另一个有用的方法是log()日志方法。此方法将字符串打印到日志窗口,并在尚未打开的情况下显示日志窗口。此功能在处理大型数据集时用于展示中间结果或状态更新很有用。要使用它,我们只需调用该方法并传入我们希望打印的字符串:
IJ.log("We finished processing "+nFiles+" file(s)!");
这假设存在一个名为nFiles的变量,它存储需要处理的文件数量。日志消息将告诉我们处理了多少文件,这取决于在插件执行时选择的文件数量。还有打开图像或获取活动图像的方法,这些方法与脚本部分中使用的示例相同(唯一的区别是需要在插件中显式声明类型)。
窗口管理器
WindowManager类是 ImageJ 中的一个实用类,它跟踪所有窗口(包括图像、结果和日志窗口),并提供允许选择特定窗口的方法。其中一些最有用的方法是getImageTitles()、getImage()和getCurrentImage()。getImageTitles方法返回一个包含所有打开图像标题的String数组。此功能非常有用,可以填充文件列表,以便用户选择要处理的特定图像。以下示例代码将展示此功能以及如何在程序中使用它:
String[] imageList = WindowManager.getImageTitles();
JComboBox jcbImages = new JComboBox(imageList);
这是一种非常用户友好的方式,允许用户选择要处理的图像。通常,ImageJ 默认使用最后打开的图像(活动图像)。当用户根据图像标题选择了一个图像时,我们可以使用getImage方法激活该图像以进行进一步处理:
ImagePlus imp;
imp = WindowManager.getImage(imageList[idx]);
这允许程序的其他部分使用指定的图像进行处理。这段代码将在关于具有用户界面的插件的章节中再次讨论。
ImagePlus
图像的主要类是ImagePlus类,它是 ImageJ 中处理图像的主要类。我们已经在之前的代码部分中简要地看到了它的调用。当调用ImagePlus类时,我们可以访问几个帮助从图像中提取信息的方法。我们还可以使用设置方法对图像进行更改:
ImageProcessor ip = imp.getProcessor();
int[] pxVal = imp.getPixel(256,256);
imp.setRoi(256,256,32,32);
此代码片段展示了几个允许您检索图像的各个方面以及设置当前图像中的区域的方法。另一个可以通过ImagePlus类访问的重要方法是ImageProcessor类,如代码片段的第一行所示。下一节将讨论这个类。
ImageProcessor
ImageProcessor类是一个允许您处理图像像素数组的类。ImageProcessor类有四个不同的子类,分别与不同的图像类型相关联:ByteProcessor用于 8 位和二进制图像,ShortProcessor用于 16 位图像,FloatProcessor用于 32 位浮点图像,以及ColorProcessor用于 RGBα图像。从ImageProcessor实例可访问的一些方法包括autoThreshold()、crop()、getPixel()和getIntArray()。这些函数允许您在图像上设置阈值,裁剪图像,在指定位置检索像素值或以数组形式获取所有像素值。
RoiManager
RoiManager类使用户能够访问 ROI 管理器和所有其功能。这个类对于检索和操作手动或程序设置的区域是必不可少的。getRoisAsArray()方法允许用户以数组的形式检索 ROI 管理器中的所有区域,这使得用户能够遍历所有区域进行测量或修改区域。以下代码是一个示例:
RoiManager rm = RoiManager.getInstance();
if (rm == null) {rm = new RoiManager();}
Roi[] regions;
regions = rm.getRoisAsArray();
for (int r=0; r<regions.length; r++) {
Roi region;
region = regions[r];
//do something...
}
建议您使用 getInstance() 方法来获取 ROI 管理器的引用。如果它返回一个 null 值,您可以使用构造函数来创建一个新的实例。在使用 getRoisAsArray() 方法后,您将得到一个类型为 Roi 的数组,其中包含一系列区域。
注意
您还可以将 regions 变量的声明和实例化合并为一条单独的语句。我更喜欢在方法或类的开头声明变量,并在有数据可用时实例化它们。在实例化或分配之前声明变量对于需要变量作用域扩展到实例化或分配点的范围之外的情况是必要的。当一个变量在循环内外使用,但值仅在循环内分配时,声明需要放在循环之外,分配在循环内。
Roi 类
Roi 类是一个泛型类,它包含 ImageJ 支持的所有区域类型。您可以使用此类检索区域的相关属性,例如使用 getBounds() 方法获取区域的边界框。您还可以使用 grow() 方法更改大小。Roi 类有几个子类,它们与 ImageJ 中可用的不同区域类型相关联。其中一些子类具有针对面积区域特定的额外方法。例如,PolygonRoi 子类有获取多边形坐标的方法,getXCoordinates() 和 getYCoordinates(),它们返回坐标的 int 数组。
应用程序编程接口
就像许多编程语言一样,ImageJ 有一个很好的应用程序编程接口(API)文档。它描述了所有可编程访问的类、方法和字段。API 参考可以在 ImageJ 网站上找到,网址为javadoc.imagej.net/ImageJ1(ImageJ1.x)、javadoc.imagej.net/ImageJ(ImageJ2)和javadoc.imagej.net/Fiji(Fiji)。API 文档是查找可用于提取相关信息的类和方法的有效方式。前几节中提到的类可以通过 API 页面找到。您还可以找到方法字段的全列表,包括方法的返回类型。在设置用于开发插件的 IDE 的章节中,我还会简要解释如何设置 Javadoc 的生成。Javadoc 是一种解析您的源代码并提取特殊格式注释以构建文档手册的方法。这可以应用于 ImageJ 源代码,从而生成一个可以离线访问的 API。我还会向您展示如何编写自己的 Javadoc 文档,然后在插件开发章节中生成您自己的代码的 API。这对于小型项目不是必需的,但对于具有复杂代码的大型项目非常有帮助,这些代码使用了大量类和方法。
设置 NetBeans IDE
现在我们将探讨如何设置一个集成开发环境(IDE),它可以用来开发 ImageJ 以及 ImageJ 的插件。Java 有很多可用的 IDE。虽然本节将向您展示如何设置一个名为 NetBeans 的特定 IDE,但许多这些设置和配置可以在您首选的 IDE 中复制。
我将要描述的设置是针对 NetBeans IDE 的,它是由管理 Java 语言的公司开发的。它可以下载不同的版本,包括用于 Java 开发、网页开发和 C++开发的版本。如果你只想为 ImageJ 开发插件,Java SE(标准版)或 Java EE(企业版)的下载应该就足够了。企业版与标准版类似,但它提供了用于多级和可扩展应用程序以及安全网络应用程序的额外 API。可以使用插件管理器(工具 | 插件)在以后扩展基本 Java 版本并添加用于网页开发或 C++编码的模块。
对于即将到来的部分,我将假设 Java SE 已经安装。然而,为了设置环境,这并不会造成差异。它可以从netbeans.org/downloads/下载。下载后,可以使用您平台的标准方法进行安装。对于 Windows 系统,有一个可以通过双击运行的安装程序。对于 OS X,有一个包含包文件的 DMG 文件,可用于安装。对于 Linux 系统,有一个 shell 脚本安装程序,某些发行版可能从它们的仓库中提供。建议您使用 NetBeans 网站上的版本,因为它比许多仓库中的版本更新。
下面的章节将描述如何在不使用项目工具的情况下开发 ImageJ1.x 插件。这种方法只需要下载一次,并作为一个独立开发平台运行。如果您希望使用 Maven 平台为 ImageJ1.x 和 ImageJ2 开发插件,可以跳过以下章节,继续到使用 Maven 开发插件部分。
收集所有组件
安装完成后,您应该能够启动 NetBeans 应用程序。第一次启动时,将有一个起始页面,允许您浏览软件并观看快速教程项目。您可以检查设置并根据您的喜好进行调整。
接下来,我们需要下载 ImageJ 的源代码。源代码可以从 ImageJ 网站imagej.nih.gov/ij/download/src/下载,在那里您将找到从版本 1.20 到最新版本(1.50a)的不同版本列表。您下载哪个版本并不重要。然而,最好使用带有最新错误修复和新增功能的最新版本。下载完成后,可以将存档提取出来,生成一个名为source的文件夹。在接下来的章节中,我将假设source文件夹的内容已提取到用户配置文件中的 Documents 文件夹内的ij/src文件夹中。这个文件夹位置将在接下来的章节中被称为source文件夹。
设置项目
这里描述的项目设置遵循rsb.info.nih.gov/ij/developer/NBTutorial.html中给出的描述,但做了一些调整。首先,当使用 NetBeans 8.0 版本时,创建项目的该方法不能正常工作。这里描述的步骤将实现相同的结果,但有一些关键更改。
第一步是在 NetBeans 中为 ImageJ 设置一个新的项目。
-
要这样做,请转到文件 | 新建项目…,这将打开以下对话框:
![设置项目]()
-
在对话框中,选择Java类别,并选择Java Free-Form Project,如图所示。然后,点击下一步 >。
-
在下一步中,我们必须选择包含源代码的文件夹。点击浏览…并选择包含提取源代码的
src文件夹。如果复制操作正确完成,剩余字段将自动填写正确的信息:![设置项目]()
-
我们现在可以点击下一步 >以继续到构建和运行操作,然后再次点击下一步而不修改字段。
-
在下一步中,我们必须设置包含我们的 ImageJ 源代码和插件源代码的位置。
-
要这样做,请在源包文件夹字段中添加
ij/src/ij和ij/src/plugins文件夹。您可以从源包文件夹中删除带有点的第一个条目。我已经将源级别设置为 JDK 1.7,这将强制 NetBeans 使用比 ImageJ 源代码构建说明中定义的新版本 Java:![设置项目]()
-
点击完成以完成设置过程:
小贴士
最后两个步骤可以保持默认设置。
项目现在将被创建,NetBeans 的主窗口将在左侧的项目选项卡中显示新项目。项目名称(ImageJ)下面有两个包源:一个用于 ImageJ 源代码(ij)和一个用于插件源代码(plugins)。
文件选项卡将显示与项目相关的文件概览:

下一个部分将查看构建 ImageJ 所需的配置。
构建 ImageJ
我们现在将设置环境以构建 ImageJ。这将使我们能够创建一个功能性的 ImageJ 程序,通过该程序我们可以执行我们的插件和宏。第一步是修改构建 ImageJ 项目时将使用的构建说明。为此,选择项目选项卡,双击 ImageJ 项目底部的build.xml文件以打开构建文件。这是一个标准的 XML 文件,可以使用 XML 语法进行编辑。要禁用代码部分,您可以使用该部分的注释标签(<!-- -->)或完全删除它。如果您希望将文件恢复到原始状态,建议使用注释方法。需要禁用的第一行是第 12 行(我正在使用注释来禁用它):
<!-- <exclude name="plugins/**"/> -->
修改后保存文件。接下来,我们将从plugins文件夹中删除两个.source文件,但不要删除.class文件。现在我们可以通过点击运行 | 构建项目(ImageJ)或按F11来开始构建 ImageJ。在构建输出窗口中可能会有一些红色警告,但现在可以忽略它们。输出结束时,应该显示构建成功。现在我们将新创建的 ImageJ 构建添加到项目中。为此,请转到文件 | 项目属性(ImageJ),然后转到Java 源文件类路径类别。首先,选择ij[ij]作为源包文件夹,然后点击添加 JAR/文件夹按钮。浏览到src文件夹,选择ij.jar文件,然后按选择按钮。对于plugins [plugins]源包文件夹重复此操作,然后按确定完成。我们现在已准备好设置开发插件的配置。
创建插件
现在我们将创建一个非常基础的插件,为使用 NetBeans 编译和调试插件做准备。首先,切换到文件选项卡,在插件文件夹上右键单击。然后,从上下文菜单中选择新建 | Java 类。在打开的对话框中,将类名设置为Plugin_Frame(或其它名称,但名称中始终包含一个下划线!)。建议为新的类创建一个包而不是默认包(我使用Template作为示例)。点击完成以创建新的 Java 源文件:

接下来,我们将以下代码放入新创建的源文件中:
import ij.ImagePlus;
import ij.plugin.filter.PlugInFilter;
import ij.process.ImageProcessor;
public class Plugin_Frame implements PlugInFilter {
protected ImagePlus imp;
public int setup(String arg, ImagePlus imp) {
this.imp = imp;
return DOES_8G | DOES_16 | DOES_32;
}
public void run(ImageProcessor ip) {
ip.invert();
}
}
这将创建一个插件,该插件将当前活动图像的 LUT 反转。接下来,保存源文件,我们将编译我们刚刚添加到源文件中的代码。要编译源代码,请转到运行 | 编译文件或按F9。
将会弹出一个窗口询问是否希望生成一个 ide-file-targets.xml 文件,因此请点击生成。将打开一个新文件,其中包含您插件的构建说明:

在ide-file-targets.xml文件中,我们将修改两行。首先,我们将第 9 行更改为以下内容:
<javac destdir="plugins" includes="${files}" source="1.7" srcdir="plugins">
我们将用plugins替换${build.classes.dir}。接下来,我们将注释掉第 8 行(或删除它):
<!-- <mkdirdir="${build.classes.dir}"/> -->
现在,保存修改后的文件并再次选择你的插件文件。我们将通过转到运行 | 编译文件或按F9来再次编译文件。在输出视图中,应该显示构建成功。接下来,我们将设置插件的调试。选择调试 | 调试项目(ImageJ),此时将弹出一个对话框,要求设置输出。点击设置输出然后点击确定以接受默认值。再次转到调试 | 调试项目(ImageJ)。这次,ImageJ 将启动,你的插件可以在插件菜单中找到。要启动你的插件,选择插件 | 模板 | 插件框架插件,你的插件应该会变得可见。
每次你想测试或更改你的代码时,记得关闭你在选择调试时创建的 ImageJ 实例。每次你选择调试项目(ImageJ)时,都会打开一个新的 ImageJ 窗口。这将使跟踪你实际调试的代码变得非常困难。
创建文档
Java 语言有一个很好的集成方式,通过在源文件中使用特殊格式的注释来创建文档。当在源文件中一致应用时,可以非常容易地创建 API 文档。在下一节中,我们将探讨如何设置文档的基础。
ImageJ Javadoc
我们将首先为 ImageJ 项目生成 Javadoc。为此,我们将选择 ImageJ 项目并转到运行 | 生成 Javadoc(ImageJ)。Javadoc 将在名为 api 的文件夹中为 ImageJ 项目生成,该文件夹位于 /ij 文件夹中。它包含一个 HTML 文件和样式文件的列表。要查看文档,只需在网页浏览器中查看 index.html 文件,你将看到 ImageJ API 文档。这个视图与我们在 API 部分看到的在线 API 非常相似,信息是相同的。通常不需要多次为 ImageJ 项目生成 Javadoc,除非你修改了文档。在下一节中,我们将探讨为你的插件创建一些 Javadoc 注释。
插件 Javadoc
要为你的插件生成 Javadoc,你需要在代码中添加一些特殊格式的注释。网上有很多关于 Javadoc 的文档,所以这里提供的信息将非常基础,但应该是一个有用的起点。首先,你需要决定需要多少文档。你可以制作出很多细节的精心制作的文档,但如果你的代码非常简单,编写文档所需的时间将比开发代码所需的时间多得多。话虽如此,拥有一些文档将有助于在一段时间后识别方法所执行的功能。
让我们看看一个简单方法的文档示例,该方法具有输入参数和输出参数。
private double[] measureParticles(Roi[] r, ImagePlus imp) {}
这是测量图像中区域集合的一些属性的基本函数定义,它返回一个测量值数组。为了包含文档,我们将在函数定义之前添加以下部分:
/**
* Take regions within an image and measure the fractal
* dimension of the provided regions.
*
* @param r Roi array containing the particles
* @param imp reference to image containing the particles
* @return array with the same dimensions as r containing
* the values for the fractal dimension.
*/
Javadoc 部分需要以一个前缀和两个星号开始。在 Javadoc 开头标签后按回车键,NetBeans 将自动生成输入参数(@param)和返回值(@return)的代码。你唯一需要添加的是参数的实际含义。
一旦你的代码已经编写了文档,你必须指示 NetBeans 构建文档化的 Javadoc 代码。为此,通过替换文件末尾现有的 javadoc 部分(它应该在文件末尾)来调整 build.xml 文件,如下所示:
<target name="javadocs" description="Build the JavaDocs.">
<delete dir="../plugins_api" />
<mkdir dir="../plugins_api" />
<javadoc
destdir="../plugins_api"
author="true"
version="true"
use="true"
windowtitle="ImageJ plugins API">
<fileset dir="." includes="**/*.java" />
</javadoc>
</target>
这将在名为 plugins_api 的文件夹中构建 ImageJ 和你的插件文档,该文件夹位于你的源数据之上的一级目录。如果你为你的插件创建了一个包,你还需要创建一个包含包信息的 package-info.java 文件。要创建此信息文件,在 项目 视图中右键单击你的包,并从上下文菜单中选择 新建 | Java 包信息…。或者,你还可以在菜单中选择 新建 | 其他…。在打开的对话框中,只需单击 确定 以接受默认值。文件将被生成,你可以在包行上方以通常的方式添加你的包文档。你需要为创建的每个包创建此信息文件。
文档编译完成后,你可以在浏览器中打开 plugins_api/index.html 文件来查看它。ImageJ 文档将首先在左上角的概览面板中显示。底部将显示你的包(s)。通过单击它们,你将看到包内定义的所有类。当你单击一个类时,你提供的文档将显示并可供浏览。
使用 Maven 开发插件
在前几节中,我讨论了如何为 ImageJ 和独立配置中的插件开发设置 NetBeans。然而,随着 ImageJ 设计的扩展,需要创建一个更模块化的方法。这种方法涉及将不同的模块构建成一个单一程序。这种模块化方法的优点是创建了一个非常灵活的应用程序,可以在未来进行扩展。缺点是它需要更多的开销来确保所有依赖项都满足以实现完全功能化的程序。这正是 Apache Maven 发挥作用的地方。Maven 是一套工具,用于描述如何将项目构建成最终程序以及所需的依赖项。
它使用一个名为 项目对象模型 (POM) 的特殊文件来完成这项工作,这是一个 XML 文件。该文件存储在项目的根目录中,并命名为 pom.xml。文件内容描述了项目的一些方面,例如一组唯一的标识符,以及项目所需依赖项的列表。当你告诉 Maven 解析 POM 文件时,它将收集所有必需的资源并编译源代码,运行指定的测试,最后将程序打包成 JAR 文件。Maven 的目标是明确地描述项目,并自动执行创建最终包所需的所有必要任务,而无需开发者手动指定每一步。这正是前几节使用 Ant 机制构建代码所描述的内容。首先,让我们看看 Maven 中 POM 的构建方式以及它是如何用于构建项目的。
POM 的构建
POM 文件描述了项目的结构。它描述了源代码的位置(默认情况下,这是 /src/main/java)和编译程序存储的构建目录(默认情况下,这是 /target)。最简 POM 文件包含以下结构:
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>some.packaged.app</groupId>
<artifactId>my-app-name</artifactId>
<version>1.0.0</version>
</project>
这个最简 POM 文件将继承来自 Super POM 文件的所有默认值。这意味着,在 POM 中未明确命名的所有内容;将使用默认值。这包括诸如源文件的位置、build 目录、构建文件类型(默认为 .jar)以及其他选项,例如用于下载源文件的仓库。对于 ImageJ1.x 插件,以下 POM 是最简描述:
<project>
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.imagej</groupId>
<artifactId>pom-imagej</artifactId>
<version>13.2.0</version>
<relativePath />
</parent>
<groupId>sc.fiji</groupId>
<artifactId>Plugin_Name</artifactId>
<version>1.0.0</version>
<name>plugins/Plugin_Name.jar</name>
<description>A Maven project implementing an ImageJ1.x plugin</description>
<properties>
<main-class>Plugin_Name</main-class>
</properties>
<dependencies>
<dependency>
<groupId>net.imagej</groupId>
<artifactId>ij</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<manifest>
<mainClass>${main-class}</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
这部分描述了使用 ImageJ 作为父项目来描述项目。这是必要的,因为我们要开发的插件需要构建 ImageJ。接下来,我们使用我们的插件名称指定了 artifactId;在这种情况下,我使用了通用的名称 Plugin_Name。在 properties 字段中,我们声明了项目的主类,即插件的名称。
注意
注意,<parent> 标签内的 <version> 标签将控制要检索哪个版本的 ImageJ1.x。使用版本 7.0.0 将检索 1.49q 版本,而 13.2.0 将检索 1.50a 版本。
接下来,我们描述了插件所需的依赖项,对于一个插件来说,是 ImageJ。最后,我们描述了构建过程,指出我们想要一个 JAR 文件。清单应该包括属性对象中 main-class 字段所描述的 main 类。此方法不需要下载任何源代码。下一节将解释如何使用 NetBeans 中的 POM 设置 ImageJ1.x 插件。
创建 Maven 插件项目
使用 Maven 项目开发插件非常简单,只需要几个基本步骤。在许多情况下,您可以使用 POM 模型的默认值,并且您只需要指定您插件(s)的名称、版本号和工件名称。我们将通过从菜单中选择 文件 | 新建项目 来使用 NetBeans 创建一个新的 Maven 项目。从类别列表中,我们将选择 Maven,然后从 项目 列表中,我们将选择 POM 项目 并点击 下一步 >:

在下一个窗口中,我们可以设置插件的主要属性。对于这个例子,我将创建一个名为 Awesome_Plugin 的虚拟插件。我将将其放置在 NetBeans 工作空间文件夹中,这是在安装 NetBeans 时创建的默认文件夹:

我添加了 组 ID 和一个版本号,但这些都很容易稍后更改。按下 完成 后,项目将被创建并添加到您的项目视图中(如果您看不到项目视图,请从菜单中选择 窗口 | 项目)。如果您展开项目,您会注意到有三个文件夹,其中目前最重要的文件夹是 项目文件。此文件夹包含我们将要编辑的 pom.xml 文件。您可以通过展开项目文件文件夹或在项目根目录上右键单击并从上下文菜单中选择 打开 POM 来打开 POM 文件进行编辑。POM 文件现在将如下所示:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tools</groupId>
<artifactId>Awesome_Plugin</artifactId>
<version>0.1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<name>Awesome_Plugin</name>
</project>
如您所见,NetBeans 向 <project> 标签添加了一些额外的属性,以标识用于此 POM 文件的 XML 架构。它还设置了 <properties> 标签,其中包含将要使用的源文件编码(UTF-8)。它还说明了将要使用的打包方式。对于插件,我们需要将其更改为 JAR。在 POM 文件中更改参数有两种方式。第一种是通过直接修改 pom.xml 文件来添加或修改标签。另一种选项是通过右键单击项目并从上下文菜单中选择 属性。这将提供一个表单,其中包含放置在 pom.xml 文件中的许多字段。在本节的剩余部分,我将假设我们直接编辑 pom.xml 文件,因为这提供了更多的灵活性,并且可以访问比属性对话框提供的更多标签。
为了说明我们的插件需要 ImageJ 存在,我们将包括 <parent> 标签及其内容,如前所述。接下来,我们将 <dependencies> 标签及其内容添加到 pom.xml 文件中。当我们现在保存 pom.xml 文件时,你可能会注意到项目视图中的文件夹结构发生了变化。现在只有两个名为 Dependencies 和 Project Files 的文件夹。你也可能注意到,Dependencies 文件夹包含两个文件:ij-1.50a.jar 和 tools.jar。这些文件是启动 ImageJ 所必需的。前者是实际的 ImageJ 程序,而后者是 ImageJ 运行所需的 jar 文件。
如果我们在这个阶段尝试构建或运行我们的项目,NetBeans 将会报错。它抱怨项目缺少构建所需的文件。这并不奇怪,因为我们还没有说明我们想要构建哪个文件。此外,我们还没有定义一个主类来运行,所以我们需要首先解决这个问题。为了说明我们的主类所在的位置,我们将 <main-class> 标签添加到 <properties> 标签中:
<main-class>Awesome_Plugin</main-class>
既然我们已经说明了主类所在的位置,我们需要指定如何构建项目。我们将使用 <build> 标签来完成,如前面所示的简化 ImageJ POM 所示。<manifest> 标签内的行描述了我们希望使用由 <main-class> 标签描述的属性中定义的主类:
<mainClass>${main-class}</mainClass>
保存 POM 文件后,我们可以尝试再次构建插件,但仍然会出错。这是因为我们仍然缺少实际的源代码。我们已经创建了一个项目描述,但还没有创建源文件。现在我们将向我们的项目中添加一个源文件,该文件必须与 <artifactId> 标签的值同名。要添加源文件,在项目视图中右键单击项目,然后选择 新建 | Java 类。这将打开 新建 Java 类 对话框:

在这个例子中,文件名需要设置为 Awesome_Plugin,因为这是我们迄今为止使用的 artifactId。我们希望放置文件的文件夹需要指定为 /src/main/java,因为这是 POM 项目中使用的默认位置。由于我没有更改此值,因此我们还需要在这里指定它。如果你更改了源文件夹的位置,你需要在新的 Java 类和 POM 文件中指定它。点击 完成 后,文件将被创建并显示在你的项目中的一个新文件夹中。已添加 Source Packages 文件夹,其中包含一个名为 <default package> 的包,该包包含你的源文件 Awesome_Plugin.java。
注意
如果你希望将插件放置在指定的包中,你可以在源文件中添加一个包声明,并要求 NetBeans 将文件移动到正确的文件夹。这可以在我们添加包声明之后完成。然后,当光标位于包声明上时,我们可以按Alt + Enter,从上下文菜单中选择将类移动到正确文件夹。本例假设我们保留了默认包。
当我们现在构建项目时,我们将看到构建成功,这意味着构建的设置是正确的。然而,当我们尝试运行项目时,我们需要提供主类:

现在的问题是,我们在这个阶段还没有主类。源代码中只有类声明,但我们还没有添加任何代码或主方法。为了解决这个问题,我们需要在源文件中添加一个主方法:
public static void main(String[] args) {
//set the plugins.dir property to make the plugin appear in the Plugins menu
Class<?>clazz = Awesome_Plugin.class;
String url = clazz.getResource("/" + clazz.getName().replace('.', '/') + ".class").toString();
int lastIdx = url.lastIndexOf('/');
String pluginsDir = url.substring(5, lastIdx);
System.setProperty("plugins.dir", pluginsDir);
// start ImageJ
new ImageJ();
}
这是一个在 Java 程序中常见的标准主方法。对于 ImageJ 插件来说,这个方法不是必需的。插件的标准入口通常是 run 方法(Plugin和PlugInFilter)或构造函数(PlugInFrame)。这个主方法仅用于 Maven 构建过程,并确保通过实例化一个新的 ImageJ 对象来启动 ImageJ。
第一行获取我们创建的插件类的引用。在下一行,我们提取了包括类文件在内的完整路径。这个 URL 将具有以下格式:file:/path/to/Awesome_Plugin.class。在下一行,我们使用lastIndexOf()方法从 URL 的开始和结束部分分别移除了file:和Awesome_Plugin部分。clazz.getName()调用将返回一个字符串,其格式如下:
-
class Awesome_Plugin -
class package.name.Awesome_Plugin
如果你为你的插件使用了包,则将使用第二种格式,而当你从插件中省略包声明时,将使用第一种格式。使用lastIndexOf()方法,我们可以在路径中包含包文件夹,从而实现无错误的编译和插件在插件菜单中的正确放置。然后,我们将包含类的文件夹添加到plugins.dir属性中。最后,我们将通过使用new关键字调用新实例来启动 ImageJ。
在这个阶段,我们有足够的代码来运行和调试我们的插件。当我们现在运行项目时,ImageJ 应该打开,插件应该在插件菜单中可见。我们可以选择它,但当我们从菜单中选择插件时可能会生成错误:

这将发生在你在类文件中使用了包定义的情况下(在我的例子中,我使用了analysis.tools包)。你可以通过在主方法的末尾添加以下行来解决此问题:
// run the plugin
IJ.runPlugIn(clazz.getName(), "");
这将在 ImageJ 启动后立即运行插件。如果您没有在类中定义包声明,您就不会遇到这个问题。因此,从开发不带包声明的源文件插件开始更容易。在接下来的章节中,我们将探讨要使插件功能化需要做什么。
创建 ImageJ2 插件
创建 ImageJ2 插件的步骤与上一节中采取的步骤非常相似。只需在<dependencies>标签内的 POM 文件中进行微小更改:
<dependencies>
<dependency>
<groupId>net.imagej</groupId>
<artifactId>imagej</artifactId>
</dependency>
</dependencies>
通过将<artifactId>标签的值从ij更改为imagej,我们指定我们希望实现一个 ImageJ2 实例。当我们保存并构建项目时,我们会看到imagej-2.0.0-rc-41.jar文件已替换了早期的ij-1.50a.jar文件。我们还需要 ImageJ2 项目的仓库:
<repositories>
<repository>
<id>imagej.public</id>
<url>http://maven.imagej.net/content/groups/public</url>
</repository>
</repositories>
需要进行的最后一个更改是在插件源代码中。我们需要使用不同的导入语句并更改启动 ImageJ 的方式:
import net.imagej.ImageJ;
[...]
public static void main(String[] args) {
[...]
// start ImageJ
final ImageJ ij = net.imagej.Main.launch(args);
}
在插件中使用的 ImageJ2 语法与 ImageJ1.x 不同,这是我们将在下一章讨论的主题。
使用 IDE 的优缺点
使用 NetBeans 等 IDE 有一些好处,可以帮助您编写代码。有自动纠正编码错误和自动导入依赖项的选项。缺点不是很大,但使用 IDE 在准备和设置方面有很多开销。无论 IDE 多么完善,它仍然不能告诉您如何解决问题。此外,在某些情况下,直接使用 Fiji 提供的脚本编辑器输入代码可能更快。IDE 也不太适合开发 ImageJ 宏,因为 ImageJ 中的宏不编译,因此不易集成到 IDE 的工作流程中。
摘要
在本章中,我们探讨了 ImageJ 中可用的宏和插件框架。我们查看了一些 ImageJ API 公开的结构,用于脚本和插件。最后,我们描述了如何设置 IDE 以开发 ImageJ 和插件,无论是作为独立项目还是基于 Maven 的项目。您还看到了如何使用 Javadoc 工具生成文档。
在下一章中,我们将探讨一些可用的插件以及它们如何解决图像处理问题。
第八章:ImageJ 插件的解剖结构
在本章中,我们将探讨 ImageJ 中插件的组织方式和在主界面中的实现方式。我们将探讨传统的插件(ImageJ1.x)以及基于 SciJava 模型的新的格式(ImageJ2)。本章将讨论以下主题:
-
ImageJ1.x 和 ImageJ2 中插件的基本结构
-
插件类型
-
实现插件
-
宏和插件的组合
-
运行和调试插件
-
可用插件的示例
插件的基本结构
ImageJ 中的插件必须遵循特定的规则。语法遵循 Java 语言,但其中一些元素是 ImageJ 特有的。在接下来的几节中,我将讨论 ImageJ1.x 插件(以下简称传统)和基于 SciJav 的新约定和结构(以下简称scijava)。请注意,当使用 scijava 模型时,运行和编译插件时必须使用 Java 1.7.x 或更高版本。此外,scijava 模型是考虑到 Maven 和 Git 系统设计的。这意味着在为 ImageJ 的未来版本开发插件时,使用这些系统是有利的。接下来的几节将探讨两种格式中插件的基本结构。
传统的插件
ImageJ1.x 中的插件必须遵循特定的规则。语法遵循 Java 语言,但其中一些元素是 ImageJ 特有的。传统的插件包括三种主要类型的插件:基本的PlugIn、PlugInFilter和PlugInFrame。接下来几节将分别介绍这些类型及其用例。
PlugIn类型
PlugIn类型用于基本插件,这些插件不需要设计上打开图像。PlugIn类型是一个 Java 接口,它只有一个需要重写的方法,即run()方法。PlugIn类型的run()方法是此类型的入口点,之后可以使用 Java 语法以任何形状或形式进行结构化。这个插件非常基础,但可以执行任何你可以设计的任务。它还可以处理图像。然而,选择图像或为处理打开图像需要程序员显式处理。此外,在处理之前检查图像类型也需要程序员显式验证。
PlugInFilter类型
此类插件在执行时需要打开一个图像,并且图像也是插件的一个输入参数。它有两个方法需要程序员重写:setup() 方法和 run() 方法。setup() 方法对图像进行基本检查,并允许插件验证当前图像是否满足处理所需的条件。它返回一个整数值,表示插件可以处理哪些类型的图像。当你想指定图像类型时,可以使用为 PlugInFilter 接口定义的字段:
-
DOES_ALL: 这些是可以处理的任何类型的图像 -
DOES_8G: 这些是 8 位灰度图像 -
DOES_16: 这些是 16 位灰度图像 -
DOES_32: 这些是 32 位浮点图像 -
DOES_RGB: 这些是 RGB 图像 -
DOES_STACKS: 这些是所有类型的栈(通道、切片或帧)
当使用 DOES_STACKS 字段时,请注意,任何多维图像都将被视为栈,处理将在图像中存在的所有通道、切片和/或帧上运行。使用此字段时,你必须进行检查以确保你的插件将处理正确的维度。如果图像不符合字段指定的类型,插件将终止并给出警告,说明图像类型不受插件支持。如果你希望支持不同类型,可以返回支持类型的总和。run() 方法是此类的主要入口点,尽管你也可以在 setup 方法中执行一些预处理。
PlugInFrame 类型
此类插件旨在为你的插件创建一个用户界面。没有需要重写的方法,唯一需要的是类的构造函数。构造函数是插件的入口点。PlugInFrame 类型扩展了 抽象窗口工具包 (AWT) 模型用于用户界面,可以填充控件或选项卡面板,以提供清晰的用户体验。此类不假设任何图像已打开,开发者需要实现所有用户界面的逻辑。
实现遗留插件
一旦您决定了插件类型,您需要做的就是实现您的插件。这听起来很简单,确实也是如此。在开始之前,您需要考虑一些事情。ImageJ 要求插件名称中包含下划线,以便在您使用单个类文件时,它可以在 插件 菜单中显示。当您将插件作为 Java 归档(JAR)文件开发时,此要求被取消。在创建您的插件时,您需要遵守 Java 语法。这意味着您需要声明和初始化您的变量。在创建函数时,您需要指定(如果有)返回类型和访问类型(公共/私有/受保护)。常规编码建议也适用于 ImageJ 插件。添加注释可能会有所帮助。您还可以使用在 第七章 中设置的 Javadoc 系统为您的函数创建文档,ImageJ 构造解释。这允许对您的代码进行全面的文档记录,并在您稍后需要修改某些内容时作为扩展记忆很有用。
在选择插件类型时,您需要考虑某些要点。当使用 PlugInFilter 时,在调用插件时将使用活动图像,导致图像被 ImageJ 锁定。当从您的插件外部对图像发出命令时,由于插件锁定,图像不可访问。这会导致出现 Image locked 错误。如果您希望在插件内部使用宏来处理图像,最好使用基本的 PlugIn(或 PlugInFrame)类型而不是 PlugInFilter 类型。在下一节中,我们将探讨 scijava 插件的构造。
结合宏和旧版插件
您还可以将宏和插件结合起来。您可以从插件内部使用运行命令来执行特定的宏或 ImageJ 命令。唯一的区别是您需要在其前面加上根类 IJ:
IJ.run("Green"); //ImageJ command
String status = IJ.runMacro("/PATH/TO/Macro"); //macro
第一行将当前活动图像和通道的查找表更改为绿色。第二行将运行由路径指定的宏。runMacro 方法返回一个包含宏的返回值的字符串,或者如果宏不返回值,则返回 NULL。如果宏被中止或遇到错误,它返回 [Aborted]。IJ 类包含一些有用的方法,允许我们运行宏和插件,以及使用打开对话框打开图像。另一个有用的方法是 IJ.log() 方法,它接受将在日志窗口中显示的字符串。这可以用于为用户提供反馈以及帮助调试插件,正如将在后面的章节中展示的那样。在 第九章,创建用于分析的 ImageJ 插件 中,我们将查看一个基本实现,其中我们在 PlugInFilter 内部结合 ImageJ 命令。
SciJava 插件
自从 ImageJ 开发以来,许多插件都是使用前面描述的遗留系统构建的。然而,遗留格式设计中的某些不足迫使 ImageJ 核心进行了重新设计。这个新框架是 SciJava 框架,其核心是 scijava-common(以及其他组件)。以下章节将描述如何在新的框架中实现插件。需要注意的是,在 SciJava 框架中开发插件的方式并不将插件分割成与遗留系统相同类型。没有需要图像或创建用户界面的插件概念。在框架中,所有插件具有相同的结构,并且它们定义了所需的组件。
@Plugin 注解
在 SciJava 框架中,插件是一个带有@Plugin注解的类。带有此注解的类会被 ImageJ 自动识别并索引,以便在用户启动插件时使用。在这个框架下,你通常会创建两种类型的插件之一:服务或命令。服务类型的插件将包含 ImageJ 内部使用的实用方法。服务提供可以在整个框架中使用的功能。另一方面,命令类型的插件旨在执行具有特定目标的特定功能。这些是用户在使用 ImageJ 界面时会遇到的插件类型:ImageJ 中的菜单项是一种命令类型插件。命令类型插件可以使用服务方法来执行常见任务,如打开图像。
无论你创建命令或服务风格的插件,它们都会在所谓的Context中运行。在 SciJava 框架中,Context描述了插件将使用的服务和命令。它充当一种沙盒。无法直接在另一个插件的上下文中使用服务和命令的方法。如果需要这样做,你必须将你的外部插件注入到你希望使用其方法的插件的上下文中。或者,你可以在上下文中使用特殊注解请求一个服务,使用@Parameter注解请求你的插件中类型的实例。例如,如果你想在插件中使用logService以允许记录事件,你可以使用以下注解:
@Parameter
private logService logService;
当插件运行时,上下文将自动生成一个logService实例,并允许你访问其方法:
public void log(String msg) {
logService.info(msg);
}
在接下来的章节中,我们将更详细地探讨@Plugin注解的两种基本类型。
服务
SciJava 框架包含大量通用服务,可用于执行基本任务和处理数据集。其中一些更重要服务包括以下服务:
-
AppService:这处理应用程序(即 ImageJ) -
EventService:这处理鼠标点击等事件 -
PluginService:这涉及可用的插件及其执行 -
DatasetService:这涉及处理图像数据的相关工具 -
OverlayService:这涉及处理叠加和 ROIs 的工具
要创建自己的服务,您需要为其创建一个上下文并定义其方法。如果您希望使用 SciJava 框架中可用的通用服务,您可以将它们作为参数添加到自己的服务中。这允许非常可扩展的代码,可以反复一致地重用。在大多数情况下,您将使用@Parameter注解在您的插件中获取这些服务的引用,从而获得其方法和功能。
命令
当您自己创建插件时,命令类型将是最常用的类型。命令描述了面向用户的插件,并描述了用户可以通过启动命令执行的操作。在创建插件时,您可以指定类型为Command类,并可以指定命令将在菜单结构中的位置:
@Plugin(type=Command.class, menuPath="Plugins>My Menu>My Plugin")
public class My_Plugin implements Command {
//code for the plugin
}
类型指定此插件关注命令接口,它按照类定义实现。menuPath参数允许您设置插件被发现时放置的菜单位置。这允许对您的插件进行精细控制和分组。在这种情况下,在 ImageJ 插件菜单中的预定义子菜单(我的菜单)内。
运行和调试插件
创建完您的代码后,您就可以编译它了。Java 不是一种解释型语言,需要将源代码编译成字节码,以便由Java 虚拟机(JVM)处理。根据您如何开发代码,有不同的方法可以采取。您可以直接使用 ImageJ,使用 Fiji 代码编辑器,或使用 NetBeans IDE。您采取的方法也取决于您是在开发遗留插件还是 scijava 插件。以下章节将首先查看遗留插件。
编译插件
由于 Fiji 基于 SciJava 框架,编译和运行插件在 vanilla ImageJ 和 Fiji 之间略有不同。此外,当使用 IDE 时,编译和运行您的插件将涉及不同的步骤。
当您使用 vanilla ImageJ 编写完插件的源代码后,您可以通过先编译它然后运行它来运行插件。要这样做,请转到插件 | 编译和运行…并选择您的插件。如果您的代码编写正确,它将编译并运行。如果在编译过程中出现任何错误,将弹出一个错误对话框,指示哪些行包含错误。大多数情况下,错误消息可能非常晦涩,并且不一定直接指向代码失败的地方。
如果您正在使用 Fiji,您可以使用脚本编辑器窗口底部的运行按钮编译和运行您的插件。编译和运行…方法在 Fiji 中不可用!任何错误消息都将显示在下面的字段中,以指示编译或运行失败的原因。
如果您正在使用 IDE 进行开发,您可以使用 IDE 的编译功能。在 NetBeans 中,您可以通过转到运行 | 编译文件或按F9来编译您的文件。如果在编译过程中没有错误,您可以通过菜单中的运行 | 运行项目或按F6来运行您的插件。如果没有发现错误,将启动 ImageJ 的新实例,在插件菜单下,您开发的插件应该会出现。语法错误将阻止编译,IDE 将使用带有白色感叹号的红色符号(以及红色波浪线)突出显示这些错误:

当指针悬停在页边距上的红色符号上方时,会给出有关错误的建议。在这种情况下,消息告诉我们期望在语句的末尾出现;。上面的符号不表示错误,而是一个警告。警告不会阻止编译或阻止插件的运行。然而,它们可能在运行时引起问题。在这个例子中,警告告诉我们使用构造函数中使用的this关键字是不建议的,可能会引起问题。对于基于 SciJava 框架的插件,程序和结果都是相同的。然而,有一些重要的事情需要考虑。下一节将简要解释一些主要点。
编译 SciJava 插件
要编译实现 SciJava 框架的插件,您需要确保您拥有所有依赖项,以及您将要运行插件的 ImageJ 框架支持该框架。对于 Fiji 来说,这不是问题。它默认已经运行在框架上。您也可以使用纯 ImageJ,但您必须确保它是 ImageJ2 版本,而不是ImageJ1.x版本。
注意
您可以通过单击主界面的状态栏来检查您正在使用哪个版本。如果它显示类似ImageJ 2.0.0-[…]的内容,则表示您正在使用 ImageJ2。如果它显示类似ImageJ 1.50a的内容,那么您正在运行ImageJ1.x版本。
由于框架的模块化特性,强烈建议您使用 Maven 工具创建和编译您的插件。这将处理构建插件所需的所有依赖项。为了使这个过程更加流畅和高效,使用支持 Maven 的 IDE 是一种最佳实践,尽管如果您愿意,也可以使用命令行界面(CLI)。为了刷新您的记忆,请参考上一章,其中解释了如何使用基于 Maven 的插件设置您的 IDE。
要使用基于 Maven 的项目使用 NetBeans 编译你的插件,你只需选择你的项目,然后转到 运行 | 构建项目 或按 F11。要启动你的插件,从菜单转到 运行 | 运行项目 或按 F6。在编译过程中遇到的问题将以与旧插件描述的类似方式显示。
调试插件
由于 ImageJ 是一个运行代码的工具,它没有很多调试代码的实用工具。但这并不意味着不可能进行一些调试。对于旧插件,你可以使用 IJ.log 方法。对于基于 SciJava 框架的插件,你可以在声明 @Parameter 之后使用 logService 并使用 info() 和 warn() 方法来创建所需服务的实例。这种调试插件方法的示例用法如下:
int someVar = 1;
int newVar = doSomething(someVar);
//legacy method
IJ.log("Old value: "+someVar+"; New value: "+newVar);
//SciJava method
logService.info("Old value: "+someVar+"; New value: "+newVar);
当使用此类方法时,包含一个简单的控制语句,如 if 语句,可能很有用。这允许你轻松地禁用或控制插件最终版本中进行的日志记录量。使用设置调试级别的全局变量,你可以控制是否显示特定的日志消息:
private static int dbglvl = 3;
...
//implement the logging based on the dbglvl value
if (dbglvl> 2) {
IJ.log("The current value is "+currValue);
}
...
//implement the logging based on the dbglvl value
if (dbglvl> 4) {
IJ.log("This statement was evaluated...");
}
在这种情况下,全局变量 dbglvl 将决定哪些消息将被显示。第一个 if 语句将以当前的调试级别(设置为 3)执行,而第二个语句在当前级别下将不会显示。在你的插件最终版本中,你可以将 dbglvl 的值更改为 1 或 0 来禁用所有低级别的调试语句。请注意,这假设 dbglvl 的较高值与较小的日志语句相关联,而较低值只会显示最基本的消息。最后,当代码运行正确时,你可能想要移除所有的 if 语句。每个语句的评估确实需要一定的时间,所以最终会减慢你的代码执行速度。
当使用 NetBeans IDE 开发插件时,有更多选项可以调试和性能分析你的代码。使用像 NetBeans 这样的 IDE 的优点是,你可以在希望停止插件执行并查看变量内容的地方设置断点。要做到这一点,请点击你希望停止的行之前的边缘。将显示一个红色方块,表示断点:

整行也会被涂成红色,以指示在运行时调试器将等待的行。记住,如果你在一个永远不会执行的语句中设置断点,调试器将永远不会停止,你的代码将无间断地运行。
要使用调试器运行代码,您可以转到调试 | 调试项目(…)或按键盘上的Ctrl + F5。当调试器遇到断点时,该行将变为绿色,您可以使用不同的单步执行功能继续操作。在 IDE 底部的变量标签页中,您将看到当前断点处可用的所有变量。请注意,您还可以评估表达式并更改当前分配给变量的值。这样做可能会导致问题或可能导致无限循环或崩溃,因此在更改值时要小心!
此外,还有一个分析器可以帮助识别在处理速度或内存使用方面效率不高的代码部分。然而,当开发简单的插件时,许多这些高级功能并非总是必要的。一旦您通过选择分析 | 分析项目(…)来启动分析器,您可以选择是否要监控 CPU 处理、垃圾回收(GC)和/或内存使用。您可以使用遥测来查看是否存在 CPU 周期过多以及垃圾回收和内存管理方面的问题。分析器的使用超出了本书的范围。然而,网上有关于如何使用和解释分析结果的一些优秀资源。
由于分析应用程序非常接近一种艺术形式,因此请谨慎使用,并且仅在您真正注意到应用程序中非常缓慢的性能或内存问题时才使用。您希望为您的插件投入多少开发开销,应始终与您获得的时间量权衡。花费数小时优化代码或算法,以便它执行快 1 秒可能并不值得,如果它只被调用一次,并且是更大命令链的一部分。然而,如果您优化在循环中调用数百次的代码,优化可能值得额外的开发时间数倍。
在下一节中,我们将探讨一些可用于科学研究的插件。
可用插件的示例
在本节中,我将讨论一些 ImageJ 可用的插件,其中大部分也已在科学期刊上发表。它们将向您展示如何使用 ImageJ 进行高级图像处理,具有不同程度的自动化和用户交互。它们还提供了一些插件设计的示例,无论是拥有自己的界面还是仅作为一个执行的单个命令。其中一些示例还提供了源代码,以便您可以了解开发者如何实现他们的算法。请注意,拥有源代码并完全理解它可能很困难:这取决于代码中的文档或注释水平。完全追踪代码的功能可能非常困难。随着程序的增长和新功能、算法的添加,它从单一核心算法偏离到更复杂的文件组。使用 IDE 中可用的 Javadoc 功能,开发者可以相对容易地创建详细的文档,使代码理解稍微容易一些。
在尝试分析源代码时,一个非常重要的点是意识到哪个文件或函数是程序的入口点。你可以确信,当代码执行时,它总是会进入这个主入口点,并且每个用户交互或函数都会在主入口点设置。不同类型插件的入口点已在前面章节中指示。此外,使用 SciJava 框架开发的插件通常有一个main()方法,但这不一定是插件的入口点。这取决于插件启动的方式。当通过 Maven 使用 IDE 启动时,main()方法用于实例化 ImageJ 并启动插件。然而,当使用例如菜单从 ImageJ 实例启动插件时,main()方法将不会被调用。
当为你的插件使用界面时,大部分动作都来自于用户按下按钮、向字段添加数字或选择一个选项。main界面只是等待用户进行操作。在 Java 中,这意味着ActionPerformed()方法成为许多算法或处理的入口点。当用户点击按钮时,这将触发一个动作事件,程序员可以使用它来捕获并连接到后续语句。首先,我们将查看一些在 ImageJ 网站上可用的插件示例(imagej.nih.gov/ij/plugins/index.html),以展示如何开发 ImageJ 解决方案来解决图像处理问题。
ImageJ 和 Fiji 中可用的示例插件
ImageJ 提供了一大批插件,这些插件扩展了核心功能。随着 ImageJ2 的到来,插件分发模型也在发生变化。在较旧的 ImageJ1.x 框架中,您需要下载一个插件的源文件或编译的.class 文件,并将其放置在插件文件夹中。当插件更新时,您需要重复整个过程。在 ImageJ2 中,提供了一个更新机制,它使用一个存储库系统。在这个系统中,ImageJ 与存储库之间的通信将确定是否有可用的更新。当有更新时,用户可以自动安装更新,而无需搜索插件。
对于大多数可用的插件,可以查看或研究源代码,以了解其他人如何解决特定的图像处理问题。以下是一些示例插件,我将描述它们旨在解决的问题或挑战。然后,我会展示一些代码来解释插件是如何尝试解决问题的。如果可以的话,请随意查看或下载源代码,以查看完整的实现。
MultipleKymograph
这样的插件示例是MultipleKymograph插件(MultipleKymograph_.java),它沿着(分割的)线选择创建一个 kymograph。它由德国海德堡的欧洲分子生物学实验室(EMBL)的 Jens Rietdorf 和 Arne Seitz 设计。它包含一组小工具和宏,可用于创建和测量 kymographs。我们已经在第五章中看到了 kymographs,使用 ImageJ 的基本测量,我们看到了它们如何可视化时间序列中的动态。在那里,我们使用Reslice命令生成 kymograph,这效果不错,但这种方法有几个小缺点。
第一个问题在于Reslice只考虑所选线上的像素。这使得它对线的放置不准确和像素噪声更加敏感。MultipleKymograph插件是一个遗留插件,它通过为用户提供一个输入字段来请求用于创建平均输出像素的线宽,试图解决这个问题。当用户在没有线选择或打开图像的情况下调用插件时,它会生成一个错误消息,指示用户在调用插件之前需要采取行动。由于 kymograph 图像的创建本身取决于像素的正确值,我将重点介绍插件是如何计算用户放置的线的平均像素强度的。
像素值的主要生成发生在sKymo(…)方法中,其定义如下:
public double[] sKymo(ImagePlus imp, ImageProcesso rip, Roi roi, int linewidth, int proflength){
int numStacks=imp.getStackSize();
int dimension = proflength*numStacks;
double[] sum = new double [dimension];
int roiType = roi.getType();
int shift=0;
int count=0;
for (int i=1; i<=numStacks; i++) {
imp.setSlice(i);
for (int ii=0;ii<linewidth;ii++){
shift=(-1*(linewidth-1)/2)+ii;
if (roiType==Roi.LINE) {
profile = getProfile(roi,ip,shift);
}
else {
profile = getIrregularProfile(roi, ip,shift);
}
for (int j=0;j<proflength;j++){
count = (i-1)*proflength+j;
sum[count]+=(profile[j]/linewidth);
}
}
}
return sum;
}
返回的sum变量包含沿堆栈的平均轮廓的结果。该方法需要五个输入参数,如下所述:
-
ImagePlus imp: 这是我们想要用于计算的源堆栈 -
ImageProcessor ip: 这是用于访问堆栈像素的图像处理器 -
Roi roi: 这是要制作成轨迹图的线的标记区域(ROI) -
int linewidth: 这是由用户输入指定的线宽 -
int proflength: 这是由用户指定的线长
方法首先声明处理所需的参数。具体来说,输出变量sum被定义为长度等于线长度乘以切片数(或帧数)的double[]向量。然后方法遍历堆栈中的切片(外层 for 循环)并使用名为getProfile()或getIrregularProfile()的方法检索轮廓。这些方法从选择中提取像素值,其中位移参数确定线相对于选择的移动距离。两者之间的唯一区别是,一个用于直线(getProfile),而另一个用于分割线(getIrregularProfile)。为了简洁起见,我将只展示前者的代码:
double[] getProfile(Roi roi,ImageProcessor ip, int shift){
double[] values;
int x1=((Line)roi).x1;
int x2=((Line)roi).x2;
int y1=((Line)roi).y1;
int y2=((Line)roi).y2;
((Line)roi).x1=x1+shift;
((Line)roi).x2=x2+shift;
((Line)roi).y1=y1+shift;
((Line)roi).y2=y2+shift;
values=((Line)roi).getPixels();
((Line)roi).x1=x1;
((Line)roi).x2=x2;
((Line)roi).y1=y1;
((Line)roi).y2=y2;
return values;
}
此方法接受用户指定的 ROI,并将其在shift参数指定的量上移动。然后它使用Roi类的getPixels()方法提取灰度值并返回它们。由于线仅由两个点定义,每个点都有一个x和y坐标,因此此方法相当简短。不规则情况要求将线沿所有定义分割线的N个坐标移动。否则,它以相同的方式工作。
ColorTransformer2
这个遗留插件在处理彩色图像时很有用,例如由数码彩色相机或网络摄像头等设备获取的图像。它最初由 Maria E. Barilla 开发为ColorTransformer,并由 Russel Cottrell 修改,形成了ColorTransformer2插件。源代码可以在www.russellcottrell.com/photo/downloads/Color_Transformer_2.java找到。
对于彩色图像,如 RGB 图像,一个问题是在 RGB 颜色空间中强度定义得不是很好。浅蓝色可能看起来比深蓝色更亮,但蓝色通道的强度值对于深蓝色可能比浅蓝色更高。为了有效地根据特定颜色对 RGB 图像进行分割,最好将其转换为一个更适合此目的的颜色空间。HSB 颜色空间将图像分为三个组成部分:色调、饱和度和亮度(有时也称为值或强度)。色调表示从红色到橙色、黄色、绿色、青色、蓝色和洋红的颜色范围。有关 HSB 颜色空间使用的详细信息,请参阅 Basic Image Processing with ImageJ 的 第二章。
此插件实现了一个 PlugInFilter,这意味着它覆盖了 setup() 和 run() 方法,这些方法构成了插件的入口点。设置方法检查是否有图像打开,并筛选出此插件可以处理的图像类型。运行方法显示一个对话框,允许用户选择要转换的颜色空间。我想在这里描述的方法是从 RGB 到 HSI 的转换,这是在基于颜色进行分割时常用的格式。
执行实际 RGB 到 HSI 转换的主要方法是 getHSI() 方法。此方法如下所示:
public void getHSI(){
for(int q=0; q<size; q++){
float var_Min = Math.min(rf[q], gf[q]); //Min. value of RGB
var_Min = Math.min(var_Min, bf[q]);
float var_Max = Math.max(rf[q], gf[q]); //Max. value of RGB
var_Max = Math.max(var_Max, bf[q]);
float del_Max = var_Max - var_Min; //Delta RGB value
c3[q] = (rf[q] + gf[q] + bf[q])/3f;
if ( del_Max == 0f ){ //This is a gray, no chroma...
c1[q] = 0f; //HSL results = 0 ? 1
c2[q] = 0f;
}
else{//Chromatic data...
c2[q] = 1 - (var_Min / c3[q]);
float del_R = (((var_Max-rf[q])/6f)+(del_Max/2f))/del_Max;
float del_G = (((var_Max-gf[q])/6f)+(del_Max/2f))/del_Max;
float del_B = (((var_Max-bf[q])/6f)+(del_Max/2f))/del_Max;
if(rf[q] == var_Max) c1[q] = del_B - del_G;
else if(gf[q] == var_Max) c1[q] = (1f/3f)+del_R-del_B;
else if(bf[q] == var_Max) c1[q] = (2f/3f)+del_G-del_R;
if (c1[q] < 0) c1[q] += 1;
if (c1[q] > 1) c1[q] -= 1;
}
}
}
这些转换基于 Color Vision and Colorimetry, Theory and Applications,D Malacara 中描述的方法。转换基于将存储在 rf、gf 和 bf 数组中的原始 RGB 值进行转换。转换后的值存储在 c1、c2、c3 和可选的 c4 数组中。对于转换到 HSI,不使用 c4 数组,因为 HSI 图像只有三个通道。例如,CMYK 颜色空间需要所有四个通道。在运行方法结束时,通道的值放置在一个新的图像中,这将变成转换后的图像。
MtrackJ
当您希望随时间跟踪对象,并且可选地在三维空间中跟踪时,此插件非常有用。它是由瑞士洛桑大学的埃里克·梅耶林格(Eric Meijering)开发的。源代码可以在 GitHub 上找到:github.com/fiji/MTrackJ/。它发表在 2012 年 2 月的 Methods in Enzymology,vol 504 上。该插件的主要界面由一组按钮组成,允许您添加、删除或移动轨迹或点,并执行测量或更改设置:

此插件的功能是在时间上跟踪物体或粒子,以确定它们的速度和方向。尽管存在自动跟踪算法,但某些数据对于自动跟踪算法来说太难或太密集,无法处理。对于这些类型的挑战,此插件将提供一个工具,可以帮助确定物体的有用参数。此插件的目标与之前描述的MultipleKymograph插件类似:测量多个物体的速度。当创建并测量轨迹时,结果将在结果窗口中显示。然后,这些结果可以用于在 ImageJ 之外直接绘图和分析,或者作为更高级分析输入。
由于这个插件功能相当全面,具有许多特性,我将重点关注一个非常微小的细节,这个细节使得该界面在跟踪物体时具有惊人的准确性。在通过跟踪按钮访问的跟踪选项中,您可以设置鼠标光标的吸附功能。这种功能可能对许多人来说都很熟悉。许多不同的应用程序都使用它来使对齐物体更加均匀和简单。当您勾选在跟踪期间应用局部光标吸附复选框时,您可以选择一个吸附功能。这个吸附功能将决定您将鼠标光标定位在要添加跟踪点的物体上时的情况。如果没有吸附,它将被放置在您点击的像素上。然而,当使用明亮质心作为吸附功能时,会发生一些有趣的事情(当使用荧光图像时)。当您通过点击添加跟踪点时,MtrackJ会确定您定义的吸附范围的质心。质心是加权强度点,不一定是单个像素,但它可以是像(x = 12.4, y = 13.45)这样的位置。对于信噪比良好的图像数据,您可以实现比光学系统提供的更好的跟踪分辨率(所谓的亚像素分辨率)。吸附坐标的位置是通过名为snapcoords()的方法计算的。我不会复制整个方法,因为它相当复杂,但我将向您展示它是如何实现明亮质心计算的:
double ox=0, oy=0;
switch (settings.snapfeature) {
//other cases skipped
case MTJSettings.BRIGHT_CENTROID: {
// Make all weights > 0:
if (minval<= 0) {
final double offset = -minval + 1;
for (int y=0; y<snaprect.height; ++y)
for (int x=0; x<snaprect.width; ++x)
snaproi[y][x] += offset;
minval += offset;
maxval += offset;
}
// Calculate Otsu threshold:
double otsu = minval;
final double maxi = OTSU_BINS;
final double range = maxval - minval;
double maxvari = -Double.MAX_VALUE;
for (int i=1; i<OTSU_BINS; ++i) {
double sum1=0, sum2=0, n1=0, n2=0;
final double thres = minval + (i/maxi)*range;
// Notice that we always have minval<thres<maxval,
// so sum1, sum2, n1, n2 are > 0 after the loop:
for (int y=0; y<snaprect.height; ++y)
for (int x=0; x<snaprect.width; ++x) {
final double val = snaproi[y][x];
if (val<thres) { ++n1; sum1 += val; }
else { ++n2; sum2 += val; }
}
final double mean1 = sum1/n1;
final double mean2 = sum2/n2;
final double vari = n1*n2*(mean1-mean2)*(mean1-mean2);
if (vari > maxvari) {
maxvari = vari;
otsu = thres;
}
}
// Calculate centroid >= threshold:
double val=0, sum=0;
for (int y=0; y<snaprect.height; ++y)
for (int x=0; x<snaprect.width; ++x) {
val = snaproi[y][x];
if (val>= otsu) {
val -= otsu;
ox += x*val;
oy += y*val;
sum += val;
}
}
ox /= sum; // sum can never be 0
oy /= sum;
break;
}
}
snapos.x = snaprect.x + ox;
snapos.y = snaprect.y + oy;
由于此插件支持多种吸附方法,因此在这个switch语句中有多个情况,为了简洁起见,我省略了它们。该方法的目标是为snapos.x和snapos.y变量分配值。对于明亮质心方法,使用基于Otsu方法的阈值。在第一个使用x和y索引的循环中,我们遍历了吸附矩形的像素,并将所有高于阈值值(thres)的像素强度(val)求和到sum2中,并将低于阈值的强度求和到sum1中。我们使用这些值来计算变化,如果它超过了矩形中的最大值,我们就调整值和 Otsu 阈值值。
在对捕捉矩形中的像素进行第二次循环时,通过将每个像素的x和y坐标乘以 Otsu 阈值以上的强度并求和来确定质心。还保持一个高于阈值的强度的累积和,并用于除以最终坐标。这些最终值用于draw()方法函数中,该函数在图像中显示明亮的质心:
public void draw(final Graphics g) { try {
if (!(g instanceofGraphics2D)) return;
final Graphics2D g2d = (Graphics2D)g;
//some code skipped for brevity...
// Draw snapping objects:
if (snapping()) {
g2d.setColor(settings.hilicolor);
try { g2d.setComposite(settings.snapopacity); } catch
(Throwable e) { }
// Snap ROI:
g2d.setStroke(settings.snapstroke);
final int slx = (int)((snaprect.x-vof.x + 0.5)*mag);
final int sly = (int)((snaprect.y-vof.y + 0.5)*mag);
final int sux = (int)((snaprect.x+snaprect.width-vof.x-0.5)*mag);
final int suy = (int)((snaprect.y+snaprect.height-vof.y-0.5)*mag);
g2d.drawLine(slx,sly,sux,sly);
g2d.drawLine(sux,sly,sux,suy);
g2d.drawLine(sux,suy,slx,suy);
g2d.drawLine(slx,suy,slx,sly);
// Snap cursor:
g2d.setStroke(settings.pointstroke);
final int xi = (int)((snapos.x - vof.x + 0.5)*mag);
final int suy = (int)((snapos.y - vof.y + 0.5)*mag);
final int hps = 6;
g2d.drawLine(xi,yi-hps,xi,yi+hps);
g2d.drawLine(xi-hps,yi,xi+hps,yi);
}
}
此方法使用由g2d引用的Graphics2D对象创建一个表示捕捉区域的正方形框(// Snap ROI部分),其大小由snaprect对象的值确定。最后,它绘制一个小+来指示由snapos.x和snapos.y变量定义的捕捉坐标(// Snap cursor部分)。
Coloc2
对于某些类型的研究问题,了解两个对象是否重叠或共定位是很重要的。Coloc2是一个由 Daniel J. White、Tom Kazimiers 和 Johannes Schindelin 开发的插件,包含在Colocalisation_Analysis.jar文件中。源代码可在 GitHub 上找到:github.com/fiji/Colocalisation_Analysis/。Coloc2 命令用于测量两个图像之间的共定位,通常表示荧光图像中的不同通道。
主要功能放在colocalise方法中,该方法使用不同的方法比较两个图像之间的像素强度。作为此插件功能的一个示例,我将查看一个更基本的函数,该函数在 ROI 管理器中存在选择并需要用于共定位分析时使用。该方法称为createMasksFromRoiManager,并调用另一个称为createMasksAndRois的第二个方法:
protected boolean createMasksFromRoiManager(int width, int height) {
RoiManager roiManager = RoiManager.getInstance();
if (roiManager == null) {
IJ.error("Could not get ROI Manager instance.");
return false;
}
Roi[] selectedRois = roiManager.getSelectedRoisAsArray();
// create the ROIs
createMasksAndRois(selectedRois, width, height);
return true;
}
protected void createMasksAndRois(Roi[] rois, int width, int height) {
// create empty list
masks.clear();
for (Roi r : rois ){
MaskInfo mi = new MaskInfo();
// add it to the list of masks/ROIs
masks.add(mi);
// get the ROIs/masks bounding box
Rectangle rect = r.getBounds();
mi.roi = new BoundingBox(
new long[] {rect.x, rect.y} ,
new long[] {rect.width, rect.height});
ImageProcessor ipMask = r.getMask();
// check if we got a regular ROI and return if so
if (ipMask == null) {
continue;
}
// create a mask processor of the same size as a slice
ImageProcessor ipSlice = ipMask.createProcessor(width, height);
// fill the new slice with black
ipSlice.setValue(0.0);
ipSlice.fill();
// position the mask on the new mask processor
ipSlice.copyBits(ipMask, (int)mi.roi.offset[0],
(int)mi.roi.offset[1], Blitter.COPY);
// create an Image<T> out of it
ImagePlus maskImp = new ImagePlus("Mask", ipSlice);
// and remember it and the masks bounding box
mi.mask = ImagePlusAdapter.<T>wrap( maskImp );
}
}
第一步是从管理器中检索 ROI,使用getSelectedRoisAsArray()方法,然后将 ROI 传递给createMasksAndRois方法。该方法将区域存储在mi.mask变量中,以便colocalise方法可以使用。此插件使用从 ImageJ2 派生的一些结构。ImagePlusAdapter是ImgLib2库中的一个包装函数。这个便利方法允许 ImageJ1.x 图像被放置在 ImageJ2 使用的ImgLib2容器中。这些函数在 ImageJ1.x 和 ImageJ2 之间的转换期间是必不可少的,并允许互操作性。接下来,我将查看一个在 SciJava 框架内开发的插件,该插件使用注解和命令和服务框架专门为 ImageJ2 设计。
Goutte_pendante
Goutte_pendante插件(悬滴)是由巴黎迪德罗大学的 Adrian Daerr 在 SciJava 框架下构建的插件。源代码可以在 GitHub 上找到:github.com/adaerr/pendent-drop。该项目使用 Maven 系统编写,因此我将简要展示pom.xml文件,作为在框架内定义插件的一个示例:
<?xml version="1.0" encoding="UTF-8"?>
<project xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>net.imagej</groupId>
<artifactId>pom-imagej</artifactId>
<version>7.1.0</version>
<relativePath />
</parent>
<groupId>name.adriandaerr.imagejplugins.pendentdrop</groupId>
<artifactId>pendent_drop</artifactId>
<version>2.0.1</version>
<name>Pendent Drop ImageJ Plugin</name>
<description>Surface tension measurement through the pendent drop method.</description>
<properties>
<main-class>Goutte_Pendante</main-class>
</properties>
<repositories>
<repository>
<id>imagej.public</id>
<url>http://maven.imagej.net/content/groups/public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.imagej</groupId>
<artifactId>imagej</artifactId>
</dependency>
</dependencies>
</project>
你可以看到,使用 POM 模型的项目描述非常简单。《标签描述了它使用了 ImageJ。依赖关系表明应使用 ImageJ2 代码库,如通过标签使用imagej而不是ij来标识 ImageJ1.x 插件。为 ImageJ2 构建的插件也倾向于有一个main方法。为了说明其功能,我将突出显示此插件main`方法中的一些代码:
public static void main(final String... args) throws Exception {
final String testImagePath =
"/home/adrian/Programmes/plugins_ImageJ_src/Traitement_Gouttes/src
/test/resources/eauContrasteMaxStack.tif";
// Launch ImageJ as usual.
//final ImageJ ij = net.imagej.Main.launch(args);
final ImageJ ij = new ImageJ();
ij.ui().showUI();
// Open test image.
final ServiceHelper sh = new ServiceHelper(ij.getContext());
final IOService io = sh.loadService(DefaultIOService.class);
final Dataset dataset = (Dataset) io.open(testImagePath);
// create a display for the dataset
final ImageDisplay imageDisplay =
(ImageDisplay) ij.display().createDisplay(dataset);
// create a rectangle
final RectangleOverlay rectangle = new
RectangleOverlay(ij.getContext());
rectangle.setOrigin(110, 0);
rectangle.setOrigin(60, 1);
rectangle.setExtent(340, 0);
rectangle.setExtent(420, 1);
rectangle.setLineColor(Colors.HONEYDEW);
rectangle.setLineWidth(1);
// add the overlays to the display
final List<Overlay> overlays = new ArrayList<Overlay>();
overlays.add(rectangle);
ij.overlay().addOverlays(imageDisplay, overlays);
for (final net.imagej.display.DataView view : imageDisplay) {
if (view instanceofnet.imagej.display.OverlayView) {
view.setSelected(true);
}
}
// display the dataset
ij.ui().show(imageDisplay);
// Launch the "CommandWithPreview" command.
ij.command().run(Goutte_pendante.class, true);
}
此代码仅在测试插件时使用,执行一些在测试代码时有用但在测试阶段之外使用插件时无用的步骤。它首先定义一个具有硬编码路径字符串的测试图像。然后执行所有 ImageJ 插件在其main方法中都会执行的步骤:启动 ImageJ 的一个实例。然后继续使用IOService打开指定的字符串指定的图像,并最终使用ImageDisplay服务显示它。此过程的结果是悬挂在孔上的液滴图像:

接下来,在打开的液滴图像上生成一个rectangle对象。此图像将被用作插件检测液滴的初始搜索空间。这是通过net.imagej.overlay包中的RectangleOverlay类完成的。最后,它将叠加层添加到显示中,并在方法的最后一句调用插件之前显示图像:
ij.command().run(Goutte_pendante.class, true);
此插件中使用的模式与上一章中描述的模式类似。然而,这里增加了额外的步骤以确保插件快速且正确地工作。如果你通过 Fiji 的更新站点安装此插件,并尝试通过从菜单中选择插件 | 液滴分析 | 悬滴立即运行它,你会得到一个错误消息。此错误消息表明在执行Goutte_pendante#paramInitializer方法时出现错误。如果你运行插件 | 液滴分析 | 关于悬滴,你将看到一个简短的说明和使用部分。在用法部分,将解释为什么它失败了。当你启动它时,没有矩形 ROI 或图像。在关于对话框的底部,有信息、文档按钮,以及检索液滴图像的方法(底部按钮)。
此插件的目标是适应液滴的形状,并且该适配形状的参数可以用来描述液体的表面张力。为了做到这一点,它需要一个描述液滴形状的类,该类在插件中定义为名为Contour的对象。它需要将多项式拟合到液滴形状上,以确定Contour参数。要做到这一点,必须确定液滴的边界。这是通过名为findDropBorders()的方法实现的。此函数将找到液滴的肩部并将位置存储在表示左右边界的数组中:
private boolean findDropBorders(ImageProcessor ip) {
leftBorder = null;
rightBorder = null;
for (int y = bounds.height - 1; y >= 0; y--) {
// find border positions with integer precision
// left border first
int xl = 0;
while (xl <bounds.width &&
ip.getPixelValue(bounds.x + xl, bounds.y + y) > threshold)
xl ++;
if (xl >= bounds.width) {// drop not detected in this scanline
if (leftBorder != null) {
leftBorder[y] = Double.NaN;
rightBorder[y] = Double.NaN;
}
continue;
} else if (leftBorder == null) {
// allocate array on drop tip detection
leftBorder = new double[y+1];
rightBorder = new double[y+1];
}
// right border next
int xr = bounds.width - 1;
while (xr> xl &&
ip.getPixelValue(bounds.x + xr, bounds.y + y) > threshold)
xr --;
xr ++; // so xl and xr point just to the right of the interface
// don't go further if not enough pixels for subpixel-fitting
if (xr - xl <= voisinage ||
xl - voisinage< 0 || xr + voisinage>bounds.width) {
leftBorder[y] = xl - 0.5;
rightBorder[y] = xr - 0.5;
continue;
}
// now determine drop borders with sub-pixel precision
leftBorder[y] = fitStep(ip, xl, y, voisinage, false);
rightBorder[y] = fitStep(ip, xr, y, voisinage, true);
} // end for y
if (leftBorder == null)
return false;
else
return true;
}
此方法执行行扫描方法以找到液滴下降低于阈值的索引。在这种情况下,液滴与背景相比具有较低的值。它从左到右的方向开始。一旦找到该像素,变量xl将不再增加,并且将小于边界宽度。这将激活else if子句,并为边界分配数组。下一步是使用相同的方法确定右侧的索引。然而,现在搜索是从右到左的方向进行的,从边界框开始,并将确定xr。
本方法中提到的代码是通用的 Java 代码,并非针对 ImageJ2 特定,但它以类似的方式完成任务。它说明了 ImageJ2 插件在开发方面不一定比其遗留版本更复杂或不同。此插件与遗留插件之间的一个区别是使用了诸如LogService接口之类的服务。当插件启动时,它使用@Parameter注解请求LogService的一个实例:
@Parameter
private LogService log;
在插件的功能部分,此服务被调用以执行错误和其他消息的记录。一个例子可以在插件的run()方法中找到:
public void run() {
HashMap<SHAPE_PARAM,Boolean> fitMe = tagParamsToFit();
if ( ! fitMe.containsValue(Boolean.TRUE) ) {
log.error("At least one parameter must be selected !");
return;
}
//code skipped for brevity...
}
日志变量可用于将消息写入日志窗口。根据使用的函数,它们将带有表示消息类型的标签。error、warn和info等方法允许报告不同类别的消息。
摘要
在本章中,我们探讨了 ImageJ1.x 和 ImageJ2 插件的解剖结构。我们还探讨了在两个框架中使用的某些特定构造。我们检查了如何使用 ImageJ 提供的工具或使用 IDE 来编译、运行和调试我们的插件。我们查看了一些已建立的插件以及它们如何实现插件以在图像处理中执行任务。
此知识将在下一章中应用,我们将从头开始创建一个插件以执行图像处理。
第九章。创建用于分析的 ImageJ 插件
在本章中,我们将探讨如何创建用于分析的插件。本章将探讨如何创建一个灵活的插件,以及如何在 ImageJ 中实现它以执行简单的分析。本章将涉及以下主题:
-
设置新的插件项目
-
使用插件处理和分析图像
-
添加用户交互和偏好
-
使用外部库
-
分享你的插件
插件背景和目标
在本节中,我将简要描述一个我们将尝试使用插件解决的问题。这个问题是一个普遍问题,在涉及活细胞或生物体的许多实验中都会遇到:它们会移动和改变形状。当我们想要量化我们所成像的细胞的一些方面时,我们需要执行三个基本步骤:
-
检测感兴趣的对象。
-
在当前帧中测量我们的对象。
-
独立检测我们时间序列中的每个对象。
这些步骤在许多涉及时间序列的不同问题中都会遇到。对于这三个步骤中的每一个,我们需要创建一个解决方案来解决问题或以有意义的方式量化对象。对于检测,我们可以考虑许多可能适合检测对象的方法。当我们回顾到第四章中讨论的主题,使用 ImageJ 进行图像分割和特征提取时,我们可能会考虑一个基于阈值的分割图像的技术,并使用粒子分析器找到具有特定特征的物体。对于测量,我们可以回到第五章,使用 ImageJ 进行基本测量,在那里我们研究了使用 ImageJ 命令测量对象的基本方法。本例的最终组件使用前两种方法对每个识别的对象进行处理。
为了使我们的插件更加通用和广泛使用,我们还需要指定一些参数,这些参数将影响每个步骤的结果。检测可能需要根据数据的不同,对有效对象有不同的标准。为此,我们可以创建一个通用的对话框,通过几个输入字段向用户请求输入。我将给出不同场景下可以使用的相同代码的不同示例。
基本项目设置
对于此项目,我将使用 Maven 系统来设置项目和所需的依赖项。大部分源代码也可以不经过这些步骤运行,但我会使用 NetBeans IDE 和 Maven POM 项目来设置它。正如我们在 第七章 ImageJ 构造解释 中所看到的,使用 Maven 为 ImageJ 设置新项目是通过导航到 文件 | 新建项目,并在向导中的 Maven 类别中选择 POM 项目 来完成的。对于此插件,我将使用项目名称 Object_Tracker。点击 完成 后,项目将被创建,并应在 项目 视图中显示。如果您看不到 项目 视图,请从菜单中选择 窗口 | 项目 来显示它。
首先,我们需要告诉 Maven 我们需要 ImageJ 作为依赖项。我们通过在 pom.xml 文件中添加一个 <dependencies> 部分来实现,正如在 第七章 ImageJ 构造解释 中所展示的那样。我们将首先查看如何创建这个插件作为遗留插件,使用所有遗留插件的标准化编码。要将它编码为遗留插件,我们将通过在 pom.xml 文件中添加以下代码,将 ImageJ 版本 1.50b 作为依赖项:
<repositories>
<repository>
<id>imagej.public</id>
<url>http://maven.imagej.net/content/groups/public</url>
</repository>
</repositories>
<dependencies>
<dependency>
<groupId>net.imagej</groupId>
<artifactId>ij</artifactId>
<version>1.50b</version>
</dependency>
</dependencies>
<repositories> 部分告诉 Maven 我们依赖项的源在哪里,在可选的 <version> 标签中,我们指定我们希望使用的 ImageJ 版本。请注意,如果您在标签中开始输入版本号,NetBeans 将建议您可以输入的版本号。在撰写本文时,1.50b 是 ImageJ 的最新版本。如果您省略此标签,版本将自动设置为最新管理的版本。我们将保存对 POM 文件的修改,这将触发 NetBeans 从存储库加载请求的依赖项,并将其放置在项目中的 依赖项 文件夹内。如果您在此阶段为项目发出构建命令(运行 | 构建项目),我们仍然会得到一个错误。我们缺少插件的源代码;这将是我们的下一步。
要添加我们的源代码,我们需要在我们的项目中添加一个新的 Java 类文件。以下步骤将帮助您创建此项目的主体类文件;然而,这些步骤与生成您想要添加到同一项目的其他类是相同的:
-
右键单击
Object_Tracker项目,从上下文菜单中选择 新建 | Java 类…。 -
将新类的名称输入为
Object_Tracker,并将 位置 设置为/src/main/java。
你将得到一个新的 Java 源文件,在 项目 视图中,你会看到 源包 目录已被添加到你的项目中。现在你可以尝试再次构建项目,这次应该可以成功完成。由于 Maven 项目也可以为项目创建 Javadoc 文档,我们还将确保我们为我们的类添加 Javadoc 注释,并为我们的插件 API 记录方法。我们将通过将其实现为 PlugInFilter 类型来开始我们插件的开发。
创建一个基本的 PlugInFilter
要创建一个 PlugInFilter 实现,我们在类名后面添加 implements 关键字,并指定 PlugInFilter 作为实现。当你使用像 NetBeans 这样的 IDE 做这件事时,它会在该语句下放置一个红色的波浪线。当你将光标放在带有波浪线的行上并按 Alt + Enter(在 NetBeans 中)时,编辑器会给你一个建议列表来纠正我们犯的错误。第一个问题是 NetBeans 找不到 PlugInFilter 符号,因为我们还没有添加它。按 Alt + Enter 并选择名为 添加导入 的 ij.plugin.filter.PlugInFilter 选项。现在你会看到导入语句已经添加到你的源文件中(通常在源文件顶部)。我们现在仍然在我们的类声明中有波浪线,因为它缺少抽象设置和运行方法的覆盖。正如在 第八章 中所解释的,《ImageJ 插件的解剖》中的 PlugInFilter 要求这两个方法存在,并且用你的初始化代码(设置)和你的编程逻辑(运行)覆盖。使用 Alt + Enter 方法,并从选择列表中选择名为 实现所有抽象方法 的选项。在这个阶段,我们有一个基本的 PlugInFilter 实现包含了所有必需的元素。
测试当前实现
目前还没有功能性代码,但让我们测试一下在这个阶段尝试运行项目会发生什么。当你从菜单中选择 运行 | 运行项目 时,你会得到一个对话框,要求选择要运行的 main 类。由于我们尚未指定 main 方法(尚无),我们无法继续,只能选择取消。我们需要做两件事:首先,我们需要在我们的源代码文件中添加一个 main 方法,其次,我们需要告诉 Maven 哪个类包含 main 方法。我们将从列表中的第一项开始。
要添加 main() 方法,我们在类的主体中某个位置添加以下代码:
public static void main(String... args) {
}
这是一个标准的 Java 风格的主方法声明,该方法接受存储在args变量中的String类型参数列表。String类型后面的三个点表示该方法可以通过可变数量的String参数调用,参数数量从零到多个。如果你希望通过命令行界面(CLI)运行你的插件,这种调用结构可能很有帮助。由于我们现在将主要忽略输入参数,所以在main方法体内使用它们并不重要。
对于第二步,我们可以以两种不同的方式修改我们的项目。我们可以在 POM 文件中的<properties>标签内输入<main-class>标签(见第七章,ImageJ 构造解释),或者我们可以使用 IDE 的功能。要编辑项目的运行方式,你可以在项目视图中右键单击项目,并从上下文菜单中选择属性。这将打开适用于此类项目的属性。从属性对话框的左侧选择运行类别:

现在,你可以看到有一个选项可以设置主类。通过按下浏览…按钮,你可以选择包含我们的main方法的Object_Tracker类。你可能还会注意到,你可以指定main方法的参数。此字段的内容将用作主方法参数args的输入参数。我们可能在稍后阶段也想输入的另一个选项是VM 选项字段。此选项将允许我们控制分配给应用程序的内存量。现在,只选择Object_Tracker作为主类。这将生成两个新的文件在 NetBeans 中,其中包含运行配置:nbactions.xml和nb-configuration.xml。或者,你可以按照第七章,ImageJ 构造解释中所述,将这些部分添加到pom.xml文件中。
注意
请注意,使用属性方法,你将限制你的应用程序使用 NetBeans 框架。如果你希望与不使用 NetBeans 的其他人交换代码,你始终应该选择纯 Maven 方法,并在pom.xml文件中直接定义你的main类。
如果你现在尝试通过导航到运行 | 运行项目来运行项目,你将不会得到任何错误,构建也将成功。唯一的问题是没有任何事情发生;我们没有看到 ImageJ,我们的插件也无法找到。我们仍然需要实现我们的主方法以确保 ImageJ 被启动。为此,我们在main方法中添加一个新的 ImageJ 实例,并保存源文件:
public static void main(String... args) {
new ImageJ();
}
通过添加ij.ImageJ的导入来修复错误后,我们运行项目,将看到 ImageJ 界面。如果你去帮助 | 关于 ImageJ,你会看到版本确实设置为1.50b。然而,当我们查看插件菜单时,我们不会在那里找到我们的插件。我们将使用第七章中展示的相同技巧,即ImageJ 构造解释,通过在调用new ImageJ()之前向我们的main方法中添加以下代码来修复我们的插件目录混乱:
/* set the plugins.dir property to make the plugin appear in the Plugins menu */
Class<?> clazz = Object_Tracker.class;
String url = clazz.getResource("/" + clazz.getName().replace('.', '/') + ".class").toString();
int lastIdx = url.lastIndexOf('/');
String pluginsDir = url.substring(5, lastIdx);
System.setProperty("plugins.dir", pluginsDir);
保存源文件并运行项目后,我们现在将在插件菜单中看到我们的插件。当你启动插件时,你会得到一个错误,指出这个方法尚未实现。这是由于抽象的setup和run方法体中只包含一个被抛出的异常(这取决于你的 NetBeans 安装和模板)。我们已经完成了插件框架,接下来,我们将实现我们的功能。
实现 setup 方法
我们将首先实现setup方法,这个方法作为一个基本的检查点,用来查看我们的插件是否能够处理当前活动的图像。我们也可以使用这个方法来进行一些准备工作,并在运行插件之前进行一些基本的检查。我们将从清除setup方法体中的当前语句开始,并添加一个返回值。setup方法要求返回一个整数值,这个值告诉 ImageJ 可以使用这个插件处理哪种类型的图像。我们还将为此函数添加一些 Javadoc 注释来解释这个函数中正在发生的事情。对于这个项目,我将假设以下结构是源代码文件的结构:
//import section
import ij.ImageJ;
//class declaration
public class Object_Tracker implements PlugInFilter {
//class-wide variables
private ImagePlus imp;
/*etc...*/
//constructor
public void Object_Tracker() {}
//main method
public static void main(String... args) {}
//setup method
public int setup(String arg, ImagePlus imp) {}
//run method
public void run(ImageProcessor ip) {}
//additional methods follow below this point
/*methods for image processing*/
}
你当然可以偏离这个模板(在 Java 语法和编程逻辑的范围内)。这种结构在 Java 文件中很常见,它包含一些不是严格必需但可能有用的元素。构造函数不是必需存在于 ImageJ 插件中。然而,添加它可能是有用的,因为它允许在你想在其他项目中调用你的插件时提高可用性。使用构造函数,你可以实现某些初始化或控制插件创建的方式。
返回类型和自动完成
我们将首先添加一个返回语句,指定我们期望处理的图像类型。对于这个项目,我们感兴趣的是在单个通道(目前)内随时间量化对象,因此我们期望处理 8 位或 16 位的堆栈。因此,我们添加以下返回语句:
return DOES_8C+DOES_8G+DOES_16;
在 IDE 中输入时,你可以使用它的自动完成功能来确定你希望返回的类型。如果你输入 DOES 并按 Ctrl + 空格键,你将得到一个可能的自动完成选项列表。你可以使用鼠标或箭头键从列表中选择一个选项,并通过双击它或按 enter 键将其插入到你输入的位置。如果选项列表非常长,你还可以在按 Ctrl + 空格键后继续输入。对于你添加的每个字符,列表将变得更加具有选择性,以匹配你输入的内容。例如,当你输入 _1 在你输入 DOES 之后,你将只得到单个选项 DOES_16。另一个很好的功能是,当你从自动完成列表中选择一个选项时,它也会显示该选择的 Javadoc。然而,你可能已经注意到这在这里不起作用;IDE 表示未找到 Javadoc。我们将在下一节中解决这个问题。
方法 Javadoc
正如我们所见,我们的 ImageJ 项目的 Javadoc 未找到。我们现在将使用 IDE 修复此问题,这只需要几个简单的步骤。首先,我们确保我们的 Javadoc 视图已打开,通过激活它。从菜单中选择 窗口 | IDE 工具 | Javadoc 文档 来激活视图。当我们把光标放在我们上面输入的 DOES_16 语句等对象上时,Javadoc 视图将显示我们在自动完成窗口中注意到的相同信息。然而,它还在底部显示一个名为 附加 Javadoc… 的链接形式的选项。当你点击它时,一个窗口会要求你提供文档的位置。还有一个名为 下载 的按钮,它将自动下载我们项目中列为依赖项的 ImageJ 版本的 Javadoc。点击确定后,你现在会看到 Javadoc 视图显示了 DOES_16 字段的文档。你还可以通过在项目视图中右键单击你的项目并从上下文菜单中选择 生成 Javadoc 来为你的项目生成 Javadoc。
我们现在将为我们的设置方法创建自己的 Javadoc 注释。使用 IDE 的最简单方法是,将光标放在设置方法上,然后按 Alt + Enter。将显示一个选项,表明 为设置创建缺失的 Javadoc,我们将选择此选项。
注意
你也可以将光标放在你想要文档化的方法上方,并输入 /**,然后按 Enter。在 NetBeans 中,输入 Javadoc 注释的开始并按 Enter 将自动完成 Javadoc 注释,并且它还会添加你方法的参数和返回类型。
选择此选项后,将在设置方法上方添加一个 Javadoc 注释,包含以下信息:
/**
*
* @param arg
* @param ip
* @return
*/
这是描述具有输入参数和返回值的方法的 Javadoc 部分的常规内容。参数被指定为@param后跟变量名。方法参数列表中的每个参数都有一个@param行。要添加有关参数的信息,你可以在变量名后开始输入(确保变量名和你的描述之间有一个空格)。参数列表上方的第一行旨在提供方法目的的简要描述。让我们添加一些关于setup方法的信息:
/**
* This is the setup method for the Object Tracker plugin
*
* @param arg input argument for control
* @param ip Currently active image
* @return DOES_8G, DOES_8C and DOES_16
*/
当你现在查看 Javadoc 查看器时,你会看到你添加的文本被显示和格式化。请注意,你可以使用标准的 HTML 标签来格式化你的文本,包括段落、标题、表格和列表。在这个阶段,你可以生成你插件的 Javadoc 并在浏览器中查看。要做到这一点,在项目视图中右键单击你的项目,并从上下文菜单中选择生成 Javadoc。在 IDE 忙于扫描项目和构建文档的等待片刻后,你可以通过从项目根目录下的target/site/apidocs/文件夹打开index.html文件来在浏览器中打开 Javadoc。或者,你也可以点击输出视图中的链接,该链接可以通过从菜单导航到窗口 | 输出来激活。结果如下:

在前面的屏幕截图中,你可以看到我们添加的文本作为描述的setup方法,底部我们看到的是我们输入的参数和返回值的详细信息。
在开发源代码时添加此类信息是一种良好的做法,这不仅有助于你在几周后回顾自己的代码,也有助于其他可能希望在自己的项目中使用或扩展你的代码的开发者。由于 Javadoc 工具负责处理和布局文档,你只需要添加方法和类的描述。我不会在本书的代码片段中明确添加文档部分,但它们将是最终源代码的一部分。在这次短暂的偏离之后,我们将返回到创建检测对象的插件。
完成设置方法
完成前几节内容后,我们现在有一个带有返回值的setup方法,表示我们将处理所有 8 位或 16 位图像和图像堆栈。现在我们将执行一些必要的检查,以确保处理完成。第一步是确保 ROI 管理器已打开,这样我们才能看到检测结果和我们的检测结果。在这个阶段,考虑我们可能想要处理的图像类型可能也是一个好主意。我们是要处理 RGB 或多通道图像和图像堆栈,还是只处理单通道图像堆栈?
我们将从检查 ROI 管理器是否可用开始。为此,我们可以使用 RoiManager 类中的 getInstance() 方法。当它尚未打开时,此方法将返回 null 值;否则,它将返回 ROI 管理器实例的引用。在返回语句之前,将以下内容添加到 setup 方法中:
if(RoiManager.getInstance() == null) {
new RoiManager();
}
如果你使用了自动完成选项来选择 RoiManager 类,NetBeans 也会自动在你的源代码文件顶部添加所需的导入语句。如果你复制并粘贴了代码,你需要自己使用 Alt + Enter 选项或手动输入来添加导入语句。
在设置过程中,剩下要做的就是检查图像类型;它需要是单通道图像,包含单个帧或切片,或者多个帧和单个切片。第一步是获取当前图像的尺寸,然后检查它是否符合我们的规格。对于当前版本的插件,我将使这些规格具有约束力,这样当它失败时,插件将不会运行。检索尺寸并检查它们是否符合我们规格的代码如下:
//get the dimensions of the current image
int[] dims = ip.getDimensions();
if (dims[2] > 1){
//more than 1 channel
return DONE;
}
else if(dims[3] > 1 && dims[4] > 1) {
//multiple slices AND frames
return DONE;
}
getDimensions() 方法返回一个长度为 5 的向量,包含宽度、高度、通道、切片和帧(按此顺序)。
在这个阶段,我想介绍 IDE 的另一个有用功能,这将使你的编码生活变得更加容易。当 IDE 添加设置和运行时的抽象方法时,它为 setup 方法中的 ImagePlus 类型以及 run 方法中的 ImageProcessor 类型都使用了 ip 参数名称。这有点令人困惑且不一致。ImagePlus 对象的惯例是使用 imp 作为引用名称,而 ip 用于 ImageProcessor 引用。我们现在将使用 IDE 中的 重构 选项来解决这个问题。
我们首先选择我们想要更改的参数;在这种情况下,setup 方法中的 ip 参数。然后我们转到上下文菜单中的 重构 | 重命名 或按 Ctrl + R。现在你会看到参数周围有一个红色框,你可以通过输入新名称来更改名称。当你现在输入 imp 时,你会看到只有与 setup 方法相关的名称被更改。这不会影响 run 方法的参数。此外,Javadoc 部分也更新以反映新的变量名称。这是一个在更改变量名称时非常有用的功能,它比搜索和替换风格的方法更有效。如果你使用了搜索和替换,run 方法中的变量名称也可能被更改,使其再次不一致。
如果我们现在运行我们的项目,我们应该在插件菜单中看到插件,但当我们启动它时,我们将收到一个NullPointerException异常。这是由于我们试图从一个不存在的图像中检索尺寸。因此,我们在调用getDimensions()方法之前需要添加一个最后的检查,以检查imp参数是否不等于null:
if (imp == null) { return DONE; }
这将确保在没有打开图像或与插件期望的图像类型不正确时不会发生任何操作。目前这并不非常用户友好。当用户激活插件时,他们期望发生某些操作。如果有一些反馈来指示为什么没有发生任何操作,那将很好。例如,我将添加一条消息,说明插件在退出之前需要打开一个栈。为此,我们在检查图像的主体中添加以下语句:
if (imp == null) {
IJ.showMessage("We need a single channel stack to continue!");
return DONE;
}
现在当你运行你的项目并启动插件时,将显示以下消息:

这使得操作更加用户友好,并避免了生成不必要的错误,这些错误可能会使用户感到困惑。编译器生成的错误和异常最多是晦涩难懂的,大多数非程序员都不会明白出了什么问题。现在我们已经完成了 setup 方法的设置,我们将现在专注于实现实际的功能代码,该代码将执行处理。
实现 run 方法
如第八章《ImageJ 插件的解剖结构》中所述,run 方法是PlugInFilter类型的入口点。在这个阶段,我们肯定有一个 8 位或 16 位的单通道栈;否则,我们永远不会达到 run 方法。现在我们可以开始实现我们的算法来检测对象。之后,我们将查看测量当前帧中对象的所需方法,最后,在存在多个对象的情况下,我们将探讨如何处理每一帧中的每个对象。我们将首先从检测开始,因为这是需要解决的主要步骤。
检测对象
为了能够检测对象,我们需要了解一些使对象可识别的属性。这听起来可能比实际情况简单。人类的视觉系统在所有类型的照明条件和情况下都能高度有效地找到对象。计算机算法才刚刚开始接近人类感觉自然的那种检测水平。在这个例子中,我将基于对象相对于背景的强度来限制对象的检测。我将假设我们希望检测的对象相对于较暗的背景是明亮的,例如在荧光成像的情况下。我们将使用Confocal Series样本图像作为练习的例子。
在我们开始使用这张图片之前,需要做一些小准备。这张图片包含两个通道,这是我们的插件的一个排除标准!因此,我们将图片分割成单独的通道,并在保存到磁盘作为 TIFF 文件之前,将其中一个通道转换为 16 位。通过使用第二章,使用 ImageJ 的基本图像处理和第三章,使用 ImageJ 的高级图像处理的知识,你应该能够执行这些步骤。我们将使用阈值来根据强度检测对象,并基于这个阈值创建一个将被添加到 ROI 管理器的选择。对于检测,我们将创建一个名为performDetection()的方法,它将从run方法中被调用。由于我们假设一个堆栈,我们还需要添加一个循环来遍历每个切片。我们将在run方法中从循环语句开始:
int nFrames = imp.getImageStackSize();
for (int f=0; f<nFrames; f++) {
imp.setSlice(f+1);
ip = imp.getProcessor();
performDetection(ip);
}
注意setSlice方法的略微异常行为。与 Java 中的数组和其他索引对象不同,图像的切片索引不是基于零的。这种古怪的行为在第二章,使用 ImageJ 的基本图像处理中已经观察到。接下来,我们创建执行检测的方法,并添加以下语句:
private void performDetection(ImageProcessor ip) { ip.setAutoThreshold(AutoThresholder.Method.Default, true);
imp.setProcessor(ip);
Roi roi = ThresholdToSelection.run(imp);
rm.addRoi(roi);
}
这使用默认方法(第一个参数)和深色背景(第二个参数)设置自动阈值。当使用自动完成选项时,许多这些值将默认填充,这使得编写代码更容易,但并不一定有助于理解。然后我们使用对类定义中添加的类级别变量的引用将新的阈值添加到当前图像中(参见前面提到的类文件模板)。
public class Object_Tracker implements PlugInFilter{
private ImagePlus imp;
private RoiManager rm;
这使得我们可以在整个类中访问当前图像和 ROI 管理器。我们还稍微修改了setup方法,以适应这些变化,使用rm引用获取实例或存储对 ROI 管理器的新引用。我们同样为类级别的ImagePlus变量(this.imp)执行相同的操作,通过存储setup方法中传入的当前图像。
rm = RoiManager.getInstance();
if(rm == null) { rm = new RoiManager();}
this.imp = imp;
要将我们的阈值对象添加到 ROI 管理器,我们使用 ImageJ 附带的一个名为ThresholdToSelection的类(另一种PlugInFilter类型)。这是当你从 ImageJ 菜单导航到编辑 | 选择 | 创建选择时被激活的类。这是一个很好的例子,说明一个插件调用了另一个插件的run方法。这意味着我们也可以在其他插件或宏中使用我们插件的run方法。
现在,我们将通过运行项目并打开我们在启动插件之前保存的一张图像来测试我们的插件。它现在应该运行通过堆栈的所有切片,并在每一帧填充 ROI 管理器。ROI 看起来相当不错,但仍然存在一些小问题。一些 ROI 中存在孔洞,有些 ROI 有与主要对象不相连的小孤立像素。在下一节中,我们将探讨使用我们在第四章学到的技术来细化检测的方法,即《使用 ImageJ 进行图像分割和特征提取》。
精细化检测
在上一节的最后测试插件时,我们注意到了当前仅使用阈值进行检测的方法的一些不足。我们看到了物体中的孔洞和希望去除的小孤立像素。这是可以使用第四章中讨论的二值处理实现的事情,即《使用 ImageJ 进行图像分割和特征提取》。现在,在我们将阈值转换为选择之前,我们将实现这种处理。第一步是使用我们的 ROI 创建一个掩码图像,我们将使用我们在第四章学到的技术来处理这个掩码图像。为了创建我们的掩码图像,我们执行以下操作:
ImagePlus impMask = new ImagePlus("mask", new
ByteProcessor(imp.getWidth(), imp.getHeight()));
impMask.setRoi(roi);
ImageProcessor ipMask = impMask.getProcessor();
ipMask.setColor(255);
ipMask.fill(impMask.getMask());
ipMask.invertLut();
此代码插入到performDetection方法中的Roi roi...语句和rm.addRoi(roi)语句之间。第一行创建了一个名为 mask 的新图像,使用ByteProcessor作为ImageProcessor;这导致了一个 8 位图像。宽度和高度被设置为与原始图像相等。当你想要在原始图像中测量对象时,这是很重要的。如果你直接从图像创建掩码,其大小将是 ROI 的边界矩形的大小。接下来,我们将 ROI 添加到新图像中,并获取该图像的ImageProcessor引用。这将允许我们修改掩码的像素。接下来,我们将前景色设置为白色(255)并用白色填充掩码。最后,我们反转二值处理的 LUT。接下来,我们将执行二值处理。我们想要填充形状中的孔洞并去除孤立像素。
我们将使用Binary插件开始填充形状中的孔洞。这是一个实现了PlugInFilter的类;然而,这次的使用略有不同。我们首先需要创建类的实例,然后设置类以适应我们的目的。我们将在上一段代码列表的最后一行下面直接添加以下代码:
Binary B = new Binary();
B.setup("fill", impMask);
B.run(ipMask);
首先,我们创建Binary类的新实例,并在我们的源代码文件顶部添加ij.plugin.filter.Binary的导入语句。接下来,我们设置插件以执行我们想要的任务,在这种情况下,填充我们的掩模中的孔。我们通过调用带有String参数("fill")和ImagePlus参数(我们的掩模图像)的setup方法来完成此操作。我们的插件具有类似的设置形式;这意味着我们也可以选择稍后实现类似系统。在最后一步,我们调用Binary插件的run方法,它将在我们的图像上执行实际处理。
接下来,我们将使用腐蚀和膨胀算子来去除孤立像素。我们将腐蚀算子运行三次,膨胀算子运行五次以创建平滑的掩模:
for (int i=0;i<3; i++) {ipMask.erode();}
for (int i=0;i<5; i++) {ipMask.dilate();}
这些值相当随意,当使用不同的图像时,其他值可能更合适。最后,我们使用ThresholdToSelection方法在我们的掩模图像上设置一个阈值,以获得一个新的 ROI,就像我们之前做的那样:
roi = ThresholdToSelection.run(impMask);
impMask.setRoi(roi);
rm.addRoi(roi);
我们再次使用roi变量,因为我们不需要在原始图像中创建的 ROI。然后我们将新的 ROI 添加到 ROI Manager 中,这是我们的检测的最后一步。如果你运行项目并在测试图像上尝试,你会看到二值处理的效应——ROI 变得更加平滑,几乎不再包含孤立像素。以下图像显示了所有 ROI 叠加在原始堆栈的第一帧上。我使用了绿色通道作为这个例子,但你也可以尝试在红色通道上运行插件。

检测多个对象
到目前为止,我们假设我们的帧中只有一个对象。现在我将研究一种允许检测多个对象的方法。为此,我们将使用我们在第三章中学习的技术,即《使用 ImageJ 的高级图像处理》。在那里,我们研究了 Z 投影以及它们如何将堆栈展平成单个图像。现在我们将使用同样的技术来定义我们的搜索空间以检测时间序列中的对象。通过创建最大强度投影,我们可以可视化对象在时间序列中某个时刻或某个时刻之后将占据的所有像素。这个投影将帮助我们定义搜索空间。对于不重叠的N个对象,你会得到N个搜索空间。
首先,我们需要创建最大强度投影。为此,我们可以使用ZProjector类,并使用MAX_METHOD将其设置为最大强度:
//create a maximum intensity projection
ZProjector zp = new ZProjector(imp);
zp.setMethod(ZProjector.MAX_METHOD);
zp.doProjection();
ImagePlus impMax = zp.getProjection();
//set a threshold in the maximum intensity projection
ImageProcessor ipMax = impMax.getProcessor();
ipMax.setAutoThreshold(AutoThresholder.Method.Default, true);
impMax.setProcessor(ipMax);
我们首先使用原始栈作为输入创建一个新的ZProjector实例。接下来,我们设置要使用的方法,并执行投影。最后,我们使用getProjection()方法检索最大强度投影图像。接下来,我们将使用ParticleAnalyzer类来检测我们最大强度投影中的对象,这些对象将定义我们的搜索空间。
要使用粒子分析器,我们创建该类的实例并设置其参数以确定搜索空间。在这个例子中,我们想要找到相对较大的对象,所以我们将为粒子设置最小尺寸限制,但不为形状设置限制。为此,我们可以使用以下代码:
//set the options for the particle analyzer
int nOpts = ParticleAnalyzer.ADD_TO_MANAGER;
int nMeasures = ParticleAnalyzer.SHOW_NONE;
double dMin = 500.0;
double dMax = Double.MAX_VALUE
//perform the particle analysis
ParticleAnalyzer pa = new ParticleAnalyzer(nOpts, nMeasures, new ResultsTable(), dMin, dMax);
RoiManager rmMax = new RoiManager(true);
ParticleAnalyzer.setRoiManager(rmMax);
pa.analyze(impMax);
//get the detected particles
Roi[] searchSpaces = rmMax.getRoisAsArray();
我们首先设置选项和我们要进行的测量。在这种情况下,我们只关心找到的对象的位置,因此我们需要在检测结束时设置 ROI(由ADD_TO_MANAGER选项指示)。测量选项设置为无,以避免生成结果或其他对象(由SHOW_NONE指示)。然后我们使用指定的选项和大小初始化粒子分析器。接下来,我们创建一个 ROI Manager 的实例,这个实例将不会显示。在我们使用analyze()方法分析图像之前,这个 ROI Manager 的实例将被分配给我们的粒子分析器。这是必要的,因为我们不想测量这些中间 ROI,我们只使用它们来识别和处理每个搜索空间。在最后一步,我们从临时的 ROI Manager 实例中提取搜索空间作为 ROI 对象。定义了搜索空间后,我们可以开始对每个搜索空间进行单独的检测。
检测可以通过与之前类似的方式创建,但有一些小的改动:我们不是使用整个图像,而是希望在单个对象的搜索空间内进行检测。我们可以通过在图像上设置搜索空间 ROI 并使用duplicate()方法进行复制来实现这一点。然后我们可以访问这个裁剪区域的像素:
//perform the detection for each search space
for (Roi searchSpace : searchSpaces) {
imp.setRoi(searchSpace);
impProcess = imp.duplicate();
for (int f = 0; f < nFrames; f++) {
impProcess.setSlice(f + 1);
ip = impProcess.getProcessor();
performDetection(impProcess, ip);
}
}
我们使用 for-each 语法对每个搜索空间都这样做,并像之前一样进行检测。还有一些其他的改动是必须的,所以请查看 Packt Publishing 网站上列出的完整代码。
实施测量
现在我们已经为每个切片确定了对象,我们可以开始测量我们的对象了。我们将使用第五章中的一些知识,即使用 ImageJ 进行基本测量,来设计针对该对象的测量方法。根据对象类型,我们可能需要考虑不同的可能重要的测量,但我将首先从我们创建的 ROI 类型的一些明显指标开始。我们的 ROI 是面积选择,因此第一个似乎相关的指标是对象(们)的面积。其他相关的测量包括对象(们)的平均强度和形状。我们将在类中添加一个单独的方法来实现这些测量。该方法将具有以下声明:
private void performMeasurements() {
Analyzer.setMeasurements(msrmnt);
imp.unlock();
rm.runCommand(imp,"Measure");
}
我们将使用 ROI 管理器中的 ROI,这样我们就不需要输入参数。我们将通过在类声明开头添加一个名为msrmnt的变量来设置测量值,这些值是我们之前讨论过的:
private static final int msrmnt = Measurements.SLICE + Measurements.AREA + Measurements.CIRCULARITY + Measurements.MEAN;
这用于将测量值设置为切片号、面积、圆形度和平均值。我们使用Analyzer类及其setMeasurements方法来获取所需的结果。最后,我们在图像上调用unlock方法,以便 ROI 管理器的宏命令能够访问我们的图像进行测量。如果您省略此语句,插件将无错误地运行,但您将不会得到任何结果。要获取结果,我们将在循环结束后直接调用我们的测量方法。在下一节中,我们将向我们的插件添加一些用户交互,允许我们更改检测中使用的某些参数。我们还将介绍 ImageJ 的偏好设置系统,以便存储我们的参数以供将来使用。
添加用户交互和偏好设置
我们迄今为止创建的插件作为一个独立插件运行良好。然而,通过允许它在包含大量数据文件的文件夹中以批处理模式运行,我们也可以很容易地增强其功能。本节将探讨需要整合的一些更改,以便它能够正常工作。通过将某些步骤设置为可以在主类实例化时调用的独立方法,我们可以以与其他类类似的方式执行特定步骤。在我们的例子中,我们以类似的方式使用了ParticleAnalyzer、ThresholdToSelection和Binary插件类。我们只需要添加一些常量和默认设置,以便这个类能够以最小配置工作。在接下来的章节中,我将向您展示一些修改,可以使这个类在与其他插件一起使用时更加灵活。
设置和选项对话框
我们插件中有几个参数会影响其行为。例如,粒子大小和阈值方法等变量会影响结果,并且需要调整以匹配数据。ImageJ 允许您设置和获取特定于您插件的偏好设置。它使用一个键值系统,使用特定的键名存储偏好设置的值。键名是一个字符串,最好对您的插件是唯一的。要设置和获取偏好设置,例如最小粒子大小,您可以使用以下语法:
Prefs.set("object_tracker.minParticleSize", 500.0);
double DMIN = Prefs.getDouble("object_tracker.minParticleSize", 500.0);
Prefs.savePreferences();
第一行显示了如何使用 object_tracker.minParticleSize 键将双精度值 500.0 存储到偏好设置中。键的命名没有严格的约定,但使用 <class name>.<key name> 构造确保键是唯一且可识别的。第二行从偏好设置中检索设置。提供的第二个值是默认值。如果键不存在,DMIN 变量将设置为该默认值(在这种情况下为 500.0)。最后,我们可以使用 savePreferences() 方法保存偏好设置。
要更改我们插件中使用的值,我们可以显示一个小对话框,允许用户输入值或进行选择。当我们使用对话框时,我们将结果保存在偏好设置中。这意味着从现在起我们可以将其作为批处理过程运行。为了让用户设置检测的关键参数,我们可以创建以下偏好设置对话框:

这是通过在 ij.gui 包中可用的 GenericDialog 类实现的。您首先创建 GenericDialog 类的一个实例,然后按照您希望它们显示的顺序将您选择的字段添加到其中。对于这个例子,我们想要设置检测模式、阈值方法、最小粒子大小和最大粒子大小。如果您愿意,可以向偏好设置中添加更多参数以提供更多灵活性。以下代码将创建一个对话框,添加字段,并显示它:
//construct and show the options dialog
GenericDialog gd = new GenericDialog("Options Object Tracker");
gd.addChoice("Detection mode", (new String[]{"multi", "single"}), DETECT_METHOD);
gd.addChoice("Threshold method", AutoThresholder.getMethods(), THRESH_METHOD);
gd.addNumericField("Min. particle size", DMIN, 0);
gd.addNumericField("Max. particle size", DMAX, 0);
gd.showDialog();
//store the values
Prefs.set("object_tracker.detectMethod", gd.getNextChoice());
Prefs.set("object_tracker.threshMethod", gd.getNextChoice());
Prefs.set("object_tracker.minParticleSize", gd.getNextNumber());
Prefs.set("object_tracker.maxParticleSize", gd.getNextNumber());
最后,我们使用键存储用户在偏好设置中选择的值。要获取值,我们使用 getNext<> 方法。这些方法按照字段添加到对话框中的顺序调用,因此 getNextChoice 的第一次调用将获取第一个选择列表的值(在这种情况下是检测模式选择)。getNextNumber 的调用将检索第一个数字字段中的数字(在这种情况下是最小粒子大小)。在 GenericDialog 实现中,字段的顺序在添加字段时变得固定,因此在检索值时需要考虑这一点。请参阅列表 9.2 以获取插件的完整代码。
添加外部库
当你为处理创建了一个插件时,你可能想要添加一些在 ImageJ 核心 API 中不可用的功能。在这种情况下,你可能想要使用一个具有你所需要功能的第三方库。如果你使用 Maven 来设置你的项目,添加一个库就像在你的 POM 文件中的<dependencies>部分列出它一样简单。作为一个例子,我将向你展示如何添加 Apache POI 库来添加一个选项,以便将我们工作的结果导出到一个 MS Excel 文件。这个库的优点是它可以在所有平台上创建.xls(x)文件,无论 MS Excel 是否已安装。我将简要向你展示如何创建一个 Excel 文件,向其中写入一些数据,然后将结果保存为.xls文件。
添加 Apache POI 的依赖项
要在 POM 文件中添加 POI 项目的依赖项,你需要在<dependencies>部分添加org.apache.poi项目。IDE 可以使用其自动完成功能帮助你完成这个过程。假设你创建了一个类似于以下所示的简单依赖项模板:
<dependency>
<groupId></groupId>
<artifactId></artifactId>
<version></version>
</dependency>
在这种情况下,你可以将光标放在<groupId>标签内,然后按Ctrl + 空格键。你将得到一个你可以选择的可能 ID 列表。当你开始输入第一部分(org.)时,你会注意到随着你继续输入,列表变得更加有限。当你输入到org.apache.po时,列表中只包含两个选项,包括 POI 包。如果你对剩余的标签重复此过程,你可能会得到以下依赖项部分:
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.13</version>
</dependency>
到目前为止,你可以开始使用这个库及其接口、类和方法来创建一个 Excel 文件(或 Word 文档和 PowerPoint 演示文稿)。请注意,用于 Excel 文件的包被指定为HSSF标识(Horrible SpreadSheet Format)。在保存 POM 文件后,你将在项目的依赖项文件夹中获得一个新的 JAR 文件。在这种情况下,是poi-3.13.jar文件,它包含 POI 项目的包。在继续之前,确保通过导航到菜单中的Run | Build Project来构建你的项目。我们现在将查看如何在下一节中实现这个库。
创建一个 Excel 文件
要创建一个 Excel 文件,我们需要使用 Apache POI 创建一个新的 Excel 工作簿实例。这通过org.apache.poi.ss中的usermodel包相对简单。我们创建一个 Workbook 接口的实例,并在一个名为saveResultsToExcel的方法中添加一个包含数据的特定名称的工作表。每次我们添加一个新的类,我们都可以通过按Alt + Enter自动添加我们的导入语句。只需确保你选择了正确的选项。如果你想添加Cell类,你有多个选项,但在这个例子中我们需要org.apache.poi.ss.usermodel.Cell的包:
public void saveResultsToExcel(String xlFile, ResultsTable rt) {
FileOutputStream xlOut;
try { xlOut = new FileOutputStream(xlFile); }
catch (FileNotFoundException ex) {
Logger.getLogger(Object_Tracker.class.getName()).log(Level.SEVERE, null, ex);
}
Workbook xlBook = new HSSFWorkbook();
Sheet xlSheet = xlBook.createSheet("Results Object Tracker");
Row r = null;
Cell c = null;
CellStyle cs = xlBook.createCellStyle();
Font f = xlBook.createFont();
Font fb = xlBook.createFont();
DataFormat df = xlBook.createDataFormat();
f.setFontHeightInPoints((short) 12);
fb.setFontHeightInPoints((short) 12);
fb.setBoldweight(Font.BOLDWEIGHT_BOLD);
cs.setFont(f);
cs.setDataFormat(df.getFormat("#,##0.000"));
cb.setDataFormat(HSSFDataFormat.getBuiltinFormat("text"));
cb.setFont(fb);
int numRows = rt.size();
String[] colHeaders = rt.getHeadings();
int rownum = 0;
//create a header
r = xlSheet.createRow(rownum);
for (int cellnum=0; cellnum<colHeaders.length; cellnum++) {
c = r.createCell(cellnum);
c.setCellStyle(cb);
c.setCellValue(colHeaders[cellnum]);
}
rownum++;
for (int row=0; row<numRows; row++) {
r = xlSheet.createRow(rownum+row);
int numCols = rt.getLastColumn() + 1;
for (int cellnum=0; cellnum<numCols; cellnum++) {
c = r.createCell(cellnum);
c.setCellValue(rt.getValueAsDouble(cellnum, row));
}
}
try { xlBook.write(xlOut); xlOut.close();}
catch (IOException ex) {
Logger.getLogger(Object_Tracker.class.getName()).log(Level.SEVERE, null, ex);
}
}
在这个例子中,我假设数据是以 ImageJ 的ResultsTable对象的形式存在的。在循环中,我们遍历行,然后逐列向每一行添加单元格。我们使用结果表的标题在 Excel 文件中创建标题。我们使用一个单独的Font对象(在这个例子中是fb)通过使其加粗来使样式与数据不同。最后,我们使用通用的FileOutputStream类将结果保存到文件中。
要获取在 ROI 管理器中按下测量按钮时生成的结果表,你可以使用以下代码:
ResultsTable rt = ResultsTable.getResultsTable();
在询问用户文件名之后,你可以调用saveResultsToExcel方法来生成一个 Excel 文件。上面的示例代码仅用于生成.xls文件。要生成.xlsx文件,你需要实现XSSFWorkbook类的电子表格。这两种 Excel 格式之间的主要区别在于一个工作表上可以包含的数据量;.xls文件每个工作表有 255 列的限制。如果你预计要生成包含更多列的表格,你需要确保使用XSSFWorkbook类。
分享你的插件
当你完成所有例程的实现并完成了(广泛的)测试后,你就可以将你的插件分发到全世界了。目前,有几种分发插件的方法可供选择,从通过电子邮件发送到 ImageJ 的自动更新机制。在这里,我将讨论后者,它具有一些很好的优点,使得它非常用户友好且高效。Fiji 和 ImageJ2 有一个系统,允许你将一个网站设置为插件源。该网站将被检查以查看是否有更新的版本可用,如果有,它将自动更新。用户需要做的只是将此网站添加到他们的更新网站列表中,以便安装和更新你的插件(们)。以下章节将描述如何设置此网站,以及用户如何将网站添加到 ImageJ(特别是 Fiji 和 ImageJ2)中。
创建一个网站
创建一个网站,你有不同的选项可用:你可以托管自己的更新网站,或者你可以使用 ImageJ Wiki 网站。现在我将专注于后者,因为它简单、免费且对每个人都是可访问的。请注意,你的用户需要使用 ImageJ2 或 Fiji 才能使用此机制。对于本节,我将假设你正在使用 Fiji,但在 ImageJ2 中它的工作方式类似。要创建网站,你可以转到菜单中的帮助 | 更新…。在打开的窗口中,按下右下角的管理更新网站按钮以获取当前可用的网站。

您可以在此窗口中按下添加我的站点按钮,然后创建一个新账户或使用现有账户。如果您已经有了账户,您只需输入用户名,如果您的密码尚未存储,也请输入密码。如果您想创建一个新账户,您可以输入一个用户名。如果它还不存在,您可以输入您的电子邮件地址并按下确定。您将收到一封电子邮件,其中包含您提供的账户的临时密码。然后您必须前往imagej.net/Special:UserLogin的 Wiki 登录页面来更改您的密码。一旦您更改了密码,您就可以在 ImageJ 的添加个人站点窗口中输入它。现在您就可以将您的插件添加到网站上了。
上传您的插件
要上传您的插件,您不能直接将文件上传到服务器。为了让它被识别为合适的更新站点和插件,需要一些额外的文件。幸运的是,ImageJ 更新器也可以为您处理这个过程。通过从菜单中选择帮助 | 更新…来打开更新器,并点击高级模式按钮。第一次上传插件时,您需要从视图选项下拉列表中选择仅查看本地文件。现在您可以在左侧选择您的插件,并在右侧的详情视图中编辑详细信息。通过右键单击您的插件,您可以打开一个上下文菜单并选择上传到我的站点。现在状态/操作列应该显示上传它,在按下应用 更改按钮并提供您的凭据后,上传将开始。
摘要
在本章中,我们从头开始使用 Maven 系统和 NetBeans IDE 开发了一个遗留插件。我们在插件中应用了之前章节中学到的一些图像处理技术。我们看到了如何为我们的插件添加一个基本用户界面,使用户能够更改影响插件功能的一些参数。我们还看到了如何将设置存储在首选项中,以便下次使用插件时可以调用它们。我们添加了一个外部库来提供 ImageJ 中不存在的一些附加功能。最后,我们探讨了自动发布我们的插件并与世界分享的方法。
在下一章中,我们将探讨可用于进一步扩展您在图像处理和项目开发方面的知识和技能的资源。
第十章。从这里走向何方?
在本章中,我们将总结之前讨论的主题,并提供一些可用的资源,以继续开发你自己的插件。本章还将探讨一些开发者可用的更高级的技术。本章将涵盖以下主题:
-
基础开发
-
额外工具
-
项目管理和反馈
-
其他资源
基础开发
在这本书中,我们探讨了执行图像处理和分析的许多不同方法。介绍了使用宏和插件的自动化,这为处理和分析提供了无限的可能性。本书中的主题是为那些希望开始开发自己的插件和宏的用户编写的。自然地,这本书在其页面上只能提供这么多信息,因此本节旨在提供一些如何继续沿着这条道路前进的线索。
首先,通过反复练习和学习之前的代码(或他人的代码),创建宏和插件会变得更容易。在创建宏时,一个好的做法是创建执行处理中单个步骤的小宏。然后,通过组合多个宏,你可以创建一个非常通用的工具箱。ImageJ 中许多有用的工具,如 ActionBar 插件,允许你以快速的方式连续启动许多不同的宏。以这种方式开发时,确保你创建可以串联的宏。一个宏的输出可以用作下一个宏的输入,依此类推。这种方法将为你节省大量时间,并允许你几乎创建任何你想要的组合。
对于插件,也有类似的建议。当你多次使用某个特定功能时,创建一个包含你常用工具的独立类可能是有价值的。通过实例化这个类来访问其方法,你可以在许多不同的项目中重用你的代码。一些较大的 ImageJ 项目使用特定的工具类或类。例如,MtrackJ 和 NeuronJ 都使用一个名为imagescience.jar的方法库。在制作具有界面的插件时,将你的实际处理或分析程序构建在单独的类中也是一个好主意,这样在开发新的或更好的技术时可以轻松地替换。
在线有许多优秀的资源可用于开发图像处理和分析中使用的宏。最基本的一个是 ImageJ 网站上的存储库,其中包含许多执行基本和更高级任务的优秀宏,可在imagej.nih.gov/ij/macros/找到。另一个好资源是 Fiji 网站,其中包含大量关于图像处理和分析的信息,以食谱的形式描述(imagej.net/Cookbook)。这个食谱基于已停用的 MBF 显微镜资源,并解释了如何使用 Fiji 提供的工具处理和分析特定图像。
还有一些非常好的资源已经出版。Burger 和 Burge 的《数字图像处理》是一本综合性的教科书,专注于 ImageJ 函数使用的算法。这为 ImageJ 中的一些处理函数提供了数学背景,以及图像变换和插值。他们工作中的示例基于或从 ImageJ 源代码中提取。另一个好资源是 Gonzalez 和 Woods 的《数字图像处理》,它使用 MATLAB 进行图像处理示例。然而,它也提供了数学背景以及图像处理中的模糊逻辑等技巧。许多示例可以作为练习转换为 ImageJ。算法的一个好来源是《数值食谱》系列书籍,该书是用 C/C++语言编写的。许多算法可以轻松地转换为 Java。另一个有助于理解图像形成物理以及信号处理基础的资源是 Steven Smith 的《科学家和工程师的数字信号处理指南》(可在www.dspguide.com/找到)。
最后,如果你希望开发利用用户界面的插件,查看许多具有界面的不同插件是个好主意。在第八章《ImageJ 插件的解剖》中,我们探讨了具有基本和高级界面的不同插件示例。其中一些示例具有非常清晰直观的界面,可以仅通过最小文档即可使用。关于良好界面设计的许多优秀出版物以及在线资源都可以展示良好和不良设计的示例。已出版的书籍示例包括《设计原理》,该书基于经验和心理学知识考察了设计中的常见概念。另一个好资源是《不要让我思考,重访版》,该书以网页设计为出发点。然而,桌面应用程序和 ImageJ 插件的核心理念是相同的。
另一个优秀的在线资源是微软 Excel 的开发者 Joel Spolsky 的博客,网址为www.joelonsoftware.com/uibook/chapters/fog0000000057.html。虽然提到的例子有些过时,但观察背后的普遍真理依然适用。它还提供了许多关于设计和运行项目的有用见解。
要了解关于新 ImageJ2 框架的插件开发,Fiji 网站上提供了关于框架(fiji.sc/ImageJ2)和某些库函数(如 ImgLib2 fiji.sc/ImgLib2)的良好文档和背景信息。还有一系列示例项目可供测试和发现如何使用这些概念,例如,如何使用 ImgLib2 处理图像(fiji.sc/ImgLib2_Examples)。GitHub 上也有可用的 ImageJ 教程集合。您可以使用您喜欢的 Git 实现从GitHub.com/imagej/imagej-tutorials/克隆它们。由于 ImageJ2 仍在积极开发中,目前处于候选发布状态,因此在最终版本发布之前,仍将实施许多更改。
其他工具
本书描述的许多示例可以使用 ImageJ 和 Fiji 的内置编辑器设计和构建,但如果您想开发更高级的插件,设置一个 IDE 会使您的工作更快。作为一个学习工具,IDE 可能不是最佳选择。IDE 本身的学习曲线可能很陡峭,而且它可能会让您变得懒惰。IDE 可以非常聪明,分析您的代码以提供自动导入类、实现所需的方法以及变量检查和转换。这些工具很方便,但了解当 IDE 建议这些选项时发生的事情很重要。当然,拼写检查和变量名补全确保您将犯更少的错误,但您永远不应该盲目依赖它。代码分析可能相当复杂,但它无法预测开发者的意图。这有时会导致非常奇怪的行为或难以调试的错误。
要使用如前几章所述的 IDE(如 NetBeans)进行工作,软件的开发者提供了许多资源。NetBeans 的教程可以在netbeans.org/kb/index.html找到,其中包含使用 IDE 开发项目的教程、示例和视频。Packt Publishing 还出版了关于 NetBeans IDE 使用的书籍,特别是《NetBeans IDE 8 烹饪书》和《精通 NetBeans》。
除了在 IDE 上投入时间外,投入时间了解和学习如何使用版本控制系统,如 Git,也是有益的。使用版本控制系统的流程与直接开发代码时略有不同。只有您定期提交更改并提供有用的提交信息时,版本控制系统才会工作。如果您是项目中的唯一开发者,您就不会遇到许多问题,如困难的代码合并和冲突,因此过程变得相对简单。借助 Git 的图形前端,整个过程变得可管理和可访问,即使是初学者也是如此。
在开始一个多开发者项目之前,请确保您熟悉使用 Git。如果涉及二进制文件,冲突和合并可能会变得复杂,解决它们可能会变得困难。许多 IDE 都内置了版本控制系统(很多时候,它在团队菜单下,即在 Eclipse 和 NetBeans 中)。无论您使用内置系统还是独立界面,在使用之前查看一些资源都是有益的。它们将帮助您了解系统能够做什么(以及不能做什么)。一个非常好的资源是Git Book,可以从git-scm.com/book/en/v2下载,同时也有印刷版。第二章基本图像处理(ImageJ)和第三章高级图像处理(ImageJ)清晰地概述了如何在日常生活中使用 Git。它主要使用命令行界面进行示例。然而,可用的图形前端使用相同的术语。Packt Publishing 还提供了关于如何使用 Git 的实用书籍,包括Git 版本控制食谱和Git:面向所有人的版本控制,它们包含开发中的实用示例和用例。
对于另一个版本控制软件包 Subversion,类似的在线资源可在svnbook.red-bean.com/en/1.7/index.html找到。它详细介绍了 Subversion 的基础知识,并解释了提交更改和版本控制过程。此资源还假设主要使用命令行方法来处理仓库系统,但术语与大多数图形前端相似。大多数与 Git 兼容的客户端也支持 Subversion,并且有可用的软件包可以将 Subversion 仓库转换为 Git 仓库,而不会(过多地)丢失仓库的结构。我经常使用的一个图形前端是 Syntevo 的 SmartGit/Hg。它适用于所有平台,并支持 GitHub 仓库。对于非商业工作它是免费的,但在用于商业项目时需要许可证。
项目管理和反馈
在项目上与团队合作时,会遇到一些困难。这种工作需要在实际开发之上增加一个额外的层次。为此,有项目管理解决方案可供选择,可以组织项目。这些解决方案通常包含多个层级,并且通常建立在版本控制系统之上或其一部分。大多数项目管理解决方案包含(至少)问题跟踪器和路线图。大多数这些解决方案主要是作为管理工具构建的,并且不直接与开发代码接口(至少在 ImageJ 插件的情况下)。然而,它们允许开发者跟踪进度并规划未来的开发,并允许最终用户提供反馈以及指出存在错误和错误的地方。大多数这些解决方案运行在可以通过浏览器访问的服务器上,并且支持多个用户以及未注册的访客用户。
存在着不同的解决方案,选择解决方案应基于你的需求和可用的设备。如今,服务器托管并不真正复杂或昂贵,因此选择范围很广。一些可用的选项包括 GitHub。它们可以免费托管项目,前提是它们是开源的。其他选项包括托管自己的解决方案,例如 Redmine。Redmine 是一个用 Ruby 语言编写的开源项目管理解决方案。其他开源示例包括 Launchpad,许多与 Ubuntu 相关的 Canonical 项目都在使用它,以及 NASA 的 JPL 和 Tor 开发所使用的 Trac。除了错误跟踪器,它们还有一些形式的时间管理和路线图。它们还允许我们托管文件和文档。Redmine 还支持项目的新闻和论坛页面,以及基于项目的基础用户管理。
如果你希望在更受管理的环境中开发代码,这些工具可能有助于调查。许多这些系统可以在小型网络或甚至仅在你的本地计算机上运行。要在更易于访问的环境中使用这些系统,你需要一个安装了版本控制系统(如 Git 或 Subversion)的计算机系统,以及一个功能齐全且配置好的网络服务器应用程序,例如 Apache、Nginx 或 Tomcat。对于 Redmine,还需要安装 Ruby 和SQL 数据库。一些系统还需要安装 PHP 以支持某些功能。尽管这听起来可能很复杂,但许多这些功能都包含在LAMP(Linux, Apache, MySQL, PHP)服务器发行版中,例如 Ubuntu 服务器版。许多服务器托管公司提供无需额外管理的完全配置好的 LAMP 服务器。此外,许多这些项目管理套件都附带简单的安装程序,可以针对许多不同的用例进行配置。
使用该软件是通过一个基本的登录页面完成的,该页面允许不同级别的访问权限的用户访问管理系统。可以授予某些用户创建、修改和管理项目的权限。其他用户可以添加为报告者。他们可以访问管理系统的大部分部分,如跟踪器和论坛页面,但不能访问项目的设置。在 Redmine 的情况下,大多数页面都是基于类似 Wiki 的语法来呈现内容的,这使得可以创建包含图形和其他布局选项的丰富和功能丰富的文档。大多数问题跟踪器都有不同的分类空间,例如错误、功能或支持。这使我们能够对报告的问题类型及其优先级进行分类。问题还可以分配给特定的开发者,使得多个开发者的分工清晰且易于管理。
Redmine 的资源和支持文档可在网上找到,地址为www.redmine.org/projects/redmine/wiki/Guide,该网站使用 Redmine 本身,并作为一个清晰的流程示例。还有一本名为《精通 Redmine》的出版书籍可供参考。
其他资源
在开发代码时,检查是否已经存在针对您问题的某些解决方案可能很有用。当已经存在并使用良好的实现时,它可以帮助您在开发库或函数时节省时间。例如,许多生成 Excel 文件的优秀解决方案都是以 Java 编写的库的形式存在的。一个这样的例子是 Apache POI 项目,它允许读取和写入 Excel 文件(以及其他 Microsoft Office 产品)。这个库的实现存在于许多软件包以及其他基于 Java 的软件中。例如,Alec de Zegher 为 Matlab 开发的 xlwrite 包装函数就使用了 Apache POI 项目来创建 Excel 文件(www.mathworks.com/matlabcentral/fileexchange/38591)。它不应与 MATLAB 提供的.xlswrite包装函数混淆,该函数使用 ActiveX 来写入 Excel 文件,因此需要 Windows 平台。
对于您希望用作库的资源,您可以使用 Maven,如第七章中所述,ImageJ 构造解释。Maven 是大多数 IDE 安装的一部分(要么作为核心部分,要么作为单独的插件)。通过在 POM 模型的<dependencies>标签中将库作为依赖项包括在内,它们将自动添加到您的项目依赖列表中。有关如何在项目中使用 Maven 的更多信息,可以在 Apache Maven 网站上找到(maven.apache.org/guides/index.html),它是 Maven 系统的主要开发者。还有一些书籍和视频可用于使用 Maven 设置和开发软件,例如,Apache Maven 3.0 烹饪书和Maven:终极指南。
摘要
在本章中,我们探讨了在完成本书后可以采取的一些步骤。我关注了一些可以使软件开发更加有组织和专业的工具。为每个组件都包括了资源,包括在线资源和已出版的作品。










浙公网安备 33010602011771号