マイクロマネージャー・プログラミングガイド
Created
on November 16, 2005 by
Nenad Amodaj
Updated for release 1.0.37(beta) on July 28, 2006
Feb. 2011 鋭意翻訳中
はじめに
マイクロマネージャーの中心的な部分は全ての自動顕微鏡制御をカプセル化した
MMCoreクラスです。
MMCore API は自動デジタル顕微鏡で使われる標準コンポーネントのすべての組み合わせを高次に抽象化しています。
MMCore API に対して書かれたプログラムは
特別な機器を使っている場合でも大抵十分に動かす事ができ、最低限の条件が満たされない最悪の場合でも、ゆるやかに機能が落ちるに留まります。
MMCore
APIの実行環境の設定検知機能を使うことで、プログラムは実行するための最低条件が満たされているかどうか自動的に検知することができます。つまり、実行に必要なデバイスがあるかどうか、それらが適応するかどうかの検知をします。
MMCore API
はプログラムの設計者にユーザーインターフェースや自動化プロトコルを機器から独立した状況で、そして最低限の労力で作り上げることを可能とさせます。
MMCoreは研究室やスクリーニングをする状況で予想されるような、顕微鏡の自動制御に必要な共通タスクを実装する方法を提供します。
ここでは「プログラム」という言葉を比較的ゆるい意味で使います。
なぜならMMCoreを使った実装は根本的に言語に依らないからです。
例えば、プログラムは複雑なユーザーインターフェースを持ったC++ソースコードのときもありますし、一連の画像を取得する、シンプルで小さなBeanshell (Javaの様な)スクリプトだったりもします。
MMCoreAPI は透明性を念頭にデザインされています。
APIのリファレンスはC++に特化している一方で、純粋に現実的な理由で、このガイドに載っている例は全てJavaプログラム言語で示されています。
しかしながら、どちらも他のプログラム環境への翻訳は直接的で容易だと思われます。
実行環境の準備
マイクロマネージャーは基本的にプラットフォーム非依存であり、プログラミング言語にも中立です。
しかしながら、実質的にはMMCoreは
Windows, Mac OS X, Linux のみで動き、
C++, Java, Matlab そして Python のプログラムで使われることを想定されています。
他の言語やOSの組み合わせで設定するには少し違ったことをする必要があります。
Java
MMCore Java
API is contained in the MMCoreJ.jar.
Any Java program using MMCore
API must have MMCoreJ.jar
in its class path. When CMMCore Java object is first created in the
calling program it will automatically attempt to load native library
MMCoreJ_wrap. This library must be visible to the Java run-time.
Default locations and exact names of libraries are platform dependent.
On Windows, native library file is MMCoreJ_wrap.dll and it must reside
either in the system path or in the current working directory of the
program in order to be detected by the Java run-time.
C++
In C++ MMCore can be used as a static library which needs to be linked
to the calling program. Interface is specified in header files MMCore.h
and Configuration.h.
Matlab
MMCore can
be used in Matlab
through its Java interface. After setting up the Java environment as
described above, MMCoreJ.jar must be added to Matlab Java class path
and the directory for the MMCore dynamic libraries (including
MMCoreJ_wrap) must be added to the system path.
Other
The current version of the MMCore does not support programming
environments other than Java, Matlab and C++. Additional support for
dynamic languages such as Python will be added in the future,
if there is enough interest .
はじめかた
You can use any editor or Java IDE of your preference to try examples
from this guide. Java run-time should be version 1.5 or higher.
CMMCore objectの作成
First thing to do is to create the CMMCore object. For example, we can
create the object and display the software version:
CMMCore core = new CMMCore();
String info = core.getVersionInfo();
System.out.println(info);
When executed, this little program should display something like:
MMCore version 1.0.16 (debug)
However, at this point MMCore
can’t do much more than that, because it
does
not know about any devices yet. In the next section we are going to try
to use a camera by loading the appropriate device adapter.
機器の読み込み
In order to control a hardware device MMCore
needs to
load the
corresponding device adapter. We use the term
“adapter”
rather than “driver” to make distinction from the
low-level
software supplied by the device manufacturer. For example, digital
cameras come with manufacturer’s drivers which need to be
installed independently from the Micro-Manager. adapters are relatively
simple software components translating specific device driver API to
common Micro-Manager plug-in interface. For simpler devices controlled
by
serial ports, there are no special manufacturer’s drivers to
install. In that case adapter is at the same time a device driver as
well.
Device adapters are packaged as dynamic libraries and CMMCore loads
them only when specifically requested by the calling program.
Let’s see how we can configure MMCore to control a camera.
The command to load device in MMCore has the following signature:
public void loadDevice(String label,
String library, String name)
throws java.lang.Exception
We can use it like this:
CMMCore core = new CMMCore();
core.loadDevice("Camera",
"DemoCamera", "DCam");
The command above has three parameters: label, library and name. Device
label is the name we want to assign to a specific device. It is
completely arbitrary and entirely up to us. We chose to call our camera
simply “Camera”. “DemoCamera”
is the dynamic
library name where the adapter resides. “DCam” is
the name
of the device adapter we want to load.
After loading the adapter the device is still inactive. Before starting
to control the device we must perform initialization:
core.initializeDevice(“Camera”);
The following program puts all of the above together: loads the camera
adapter, initializes it and additionally snaps an image with exposure
set to 50ms.
CMMCore core = new CMMCore();
core.loadDevice("Camera", "DemoCamera", "DCam");
core.initializeDevice("Camera");
core.setExposure(50);
core.snapImage();
byte image[] = (byte[]) core.getImage();
long width = core.getImageWidth();
long height = core.getImageHeight();
For clarity the above example omits some details necessary to really
compile and run this example. Complete Java code is here: Tutorial1.java
Error handling
If an error occurs during execution of the function call, CMMCore
object will throw a java exception, which you can handle in any
standard way. For example:
try {
core.loadDevice("Camera", "Hamamatsu",
"Hamamatsu_DCAM");
core.initializeAllDevices();
} catch (Exception e){
System.out.println("Exception: " +
e.getMessage() + "\nExiting now.");
System.exit(1);
}
Using device properties
Each device loaded into the MMCore will expose a number of properties
which we can read or change. A property is simply a named tag, or a
field consisting of a name and value.
// get some properties
String propBinning =
core.getProperty("Camera", "Binning");
String propPixelType =
core.getProperty("Camera", "PixelType");
// set some properties
core.setProperty("Camera", "Binning",
"4");
core.setProperty("Camera",
"PixelType", "16bit");
To get or set the property you have to supply two parameters: device
label and property name. "getProperty" method will return the value,
while in the "setProperty" method you have to supply the value as an
additional, third parameter. All property values are always treated as
strings regardless of the actual data type they represent.
How do you find out which properties are supported by a particular
device? This code prints all properties of the "Camera" and their
current values:
StrVector properties = core.getDevicePropertyNames("Camera");
for (int i=0; i<properties.size(); i++) {
String prop = properties.get(i);
String val = core.getProperty("Camera",
prop);
System.out.println("Name: " + prop + ",
value: " + val);
}
Note the use of the StrVector class as a simple vector containing of
strings
(String class) containing property names. This class is defined within
MMCoreJ.jar.
How do you find out which values are valid for a particular property?
This code prints all allowed values for the "PixelType" property:
StrVector values = core.getAllowedPropertyValues("Camera", "PixelType");
for (int i=0; i<values.size(); i++) {
String val = values.get(i);
System.out.println(val);
}
If the getAllowedProperties() call returns empty vector, it means that
the range of possible values is so broad that it is not practical to
enumerate them, or that the device adapter does not have this
information. You could interpret that as if any value is allowed.
However, it is not guaranteed that the device will be able to accept
any particular value in a given context. On the other hand, if the
returned vector is not empty you can expect that setting any of the
allowed values will succeed.
Some properties are read-only. For example, you can discover if the
camera property "Description" is read-only by using:
boolean ro = core.isPropertyReadOnly("Camera", "Description");
Complete Java code is here: Tutorial2.java.
Relationship between properties and device specific calls
By examining the two examples Tutorial1.java
and Tutorial2.java
you can
get an insight on two different ways to control devices: device
specific API and generalized property mechanism.
Device specific API consists of commands which imply device of certain
type. This API reflects capabilities expected from and any automated
microscope, regardless of specific devices used to build it. For
example:
// commands which imply a single device attached to the system,
// in this case a camera.
core.snapImage();
long h = core.getImageHeight();
double e = core.getExposure();
// example commands which imply a specific device type
// (device label must be provided)
core.setPosition("Z", 120.0); // works only for stages
core.setState("F1",
3); //
works only for filter wheels, shutters, etc.
On the other hand, property mechanism is very general and does not
assume anything about the device. In this way you can use a very
flexible conceptual model in which the entire system is just a
collection of various devices and each device has a number of property
tags which you read or change. The property mechanism makes possible to
build robust user interfaces and programs which automatically
re-configure based on specific run-time configuration of the system.
// these two commands
core.setPosition("Z", 123.0);
core.setExposure("Camera", 55.0);
// have exactly the same effect as
core.setProperty("Z", "Position", "123.0");
core.setProperty("Camera", "Exposure", "55.0");
Also, property mechanism is allows us to control many details which are
very specific to the device type. For example, some cameras will have
"Offset" property available and some will not.
Working with multiple devices
Let us consider a somewhat more complicated system with four devices:
camera, shutter, and two filter wheels.
// clear previous setup if any
core.unloadAllDevices();
// load devices
core.loadDevice("Camera", "DemoCamera", "DCam");
core.loadDevice("Emmision", "DemoCamera", "DWheel");
core.loadDevice("Exictation", "DemoCamera", "DWheel");
core.loadDevice("Shutter", "DemoCamera", "DWheel");
core.loadDevice("Z", "DemoCamera", "DStage");
// initialize
core.initializeAllDevices();
// list devices
StrVector devices = core.getLoadedDevices();
System.out.println("Device status:");
for (int i=0; i<devices.size(); i++){
System.out.println(devices.get(i));
// list device properties
StrVector properties =
core.getDevicePropertyNames(devices.get(i));
for (int j=0;
j<properties.size(); j++){
System.out.println(" " +
properties.get(j) + " = "
+
core.getProperty(devices.get(i), properties.get(j)));
StrVector values =
core.getAllowedPropertyValues(devices.get(i),
properties.get(j));
for (int k=0; k<values.size();
k++){
System.out.println("
" + values.get(k));
}
}
Complete Java code is here: Tutorial3.java.
Using serial ports
Many devices use serial ports for communication with the host computer.
For MMCore serial port is also a device with number of properties to
manipulate.
core.loadDevice("Port", "SerialManager", "COM1");
core.setProperty("Port", "StopBits", "2");
core.setProperty("Port", "Parity", "None");
core.initializeDevice("Port");
Once the property is loaded and initialized, you can send and receive
terminated command strings like this:
core.setSerialPortCommand("Port", "MOVE X=300", "\r");
String answer = core.getSerialPortAnswer("Port", "\r");
The last parameter in both send and receive commands is a terminating
sequence, in this case carriage return. The receive command
getSerialPortAnswer() will wait until it detects the terminating
sequence or times out.
Most of the devices use similar protocol and therefore in principle you
can control any device supporting serial port communication even if you
do not have device adapter. Just send and receive commands through the
serial port. Of course, this way you won't be able to use many of the
more advanced features of the Micro-Manager system (such as device
synchronization, metadata) and your code will not portable.
Using State devices
State device is any device with a relatively small number of discrete
states. Most common examples of state device are filter switchers
(wheels) and objective turrets.
core.loadDevice("Emission", "DemoCamera", "DWheel");
core.loadDevice("Excitation", "DemoCamera", "DWheel");
core.loadDevice("Dichroic", "DemoCamera", "DWheel");
core.loadDevice("Objective", "DemoCamera", "DObjective");
core.initializeAllDevices();
// set emission filter to position 2
core.setState("Emission", 2);
// verify position
core.waitForDevice("Emission"); // until it stops moving
long state = core.getState("Emission");
Almost invariably state devices serve as placeholders for
interchangeable equipment: objectives or filters. To make code more
readable, and more device independent, instead of just using position
numbers as shown above it is much better to refer to positions with
meaningful names such as "Nikon S Fluor 10X" or "Chroma-D360". State
devices support position labeling feature and any position can be
assigned with an arbitrary name:
// define emission filter positions
core.defineStateLabel("Emission", 0, "Chroma-D460");
core.defineStateLabel("Emission", 1, "Chroma-HQ620");
core.defineStateLabel("Emission", 2, "Chroma-HQ535");
core.defineStateLabel("Emission", 3, "Chroma-HQ700");
// set position using label
core.setStateLabel("Emission", "Chroma-D460");
// verify position
core.waitForDevice("Emission"); // until it stops moving
String stateLabel = core.getStateLabel("Emission");
The state device used in tutorial examples is really a software
simulator (from the DemoCamera adapter library) and it does not use any
hardware connections. But most state devices are controlled through
serial ports, so in order to control them you'll need to link state
device to appropriate serial port device:
core.unloadAllDevices();
// setup serial port
core.loadDevice("P1", "SerialManager", "COM1");
core.setProperty("P1", "StopBits", "1");
// setup filter wheels
core.loadDevice("WA", "SutterLambda", "Wheel-A");
core.setProperty("WA", "Port", "P1");
core.loadDevice("WB", "SutterLambda", "Wheel-B");
core.setProperty("WB", "Port", "P1");
mmc.initializeAllDevices();
The code above looks fairly straightforward, but there are a couple of
important points to consider here. First, note that Micro-Manager
device adapters do not take control of serial ports directly.
We
first
loaded serial port device "P1" on COM1 and then we just supplied the
filter wheel devices "WA" and "WB" with the port label. Devices will
use port labels through MMCore internal mechanisms to transmit and
receive data from ports, and will not be able to lock them or take
control of them.
You will notice that both devices "WA" and "WB" appear to be connected
to the same port "P1". That's because they physically belong to the
same controller box which uses single port to control multiple devices.
This is a relatively common situation, but having each filter on a
different port is also fine.
In all examples from previous sections we started manipulating
properties only after we initialized the system either by
initializeDevice() or initializeAllDevices(). In fact, most of the
device properties are even not available before the device has been
initialized. But, in the example above we set port related properties
before initializing the system. We had to do it that way because these
properties must be specified correctly in advance in order for
initialization to succeed. Which properties, if any, are required or
accessible before initialization depends on the specific device
adapter. Camera adapters, for example, usually do need or expose any
pre-initialization properties.
The order in which devices are loaded will be the same order in which
they are going to be initialized in the initializeAllDevices() command.
Therefore, port devices should be loaded before other devices using
them.
Using configurations
In practical situations we often need to execute groups of multiple
commands over an over again. For example, to set the correct light path
for imaging DAPI fluorescence channel you need to set three filters in
proper positions:
// Set DAPI imaging path
core.setState("Emission", 1);
core.setState("Excitation", 2);
core.setState("Dichroic", 0);
Or equivalently, by exploiting property mechanism:
// equivalent to above
core.setProperty("Emission", "State", "1");
core.setProperty("Excitation", "State", "2");
core.setProperty ("Dichroic", "State", "0");
To simplify programming Micro-Manager provides configuration feature in
which you can define groups of commands and execute them as a single
command. To define "DAPI" configuration (group of commands) you need to
write:
// Define DAPI configuration once at the beginning of the session
core.defineConfigGroup("Channel","DAPI", "Emission", "State", "1");
core.defineConfigGroup("Channel","DAPI", "Excitation", "State", "2");
core.defineConfigGroup("Channel","DAPI", "Dichroic", "State", "0");
// use configuration command many times
core.setConfig("DAPI);
Each time you execute setConfiguration("DAPI"), the three filters will
be set to the defined positions. There is no limit in the number of
devices or number of different properties you include in one
configuration. You can set objectives, filters, stage positions, camera
parameters in a single configuration command. Typically predefined
configurations are automatically defined once at the initialization
(when the program starts up) phase and used thereafter in the
acquisition scripts or interactively.
To discover which configurations are currently defined in your system
and what exact settings they consist of, you can use:
StrVector configs = core.getAvailableConfigGroups();
for (int i=0; i<configs.size(); i++){
Configuration cdata =
core.getConfigData(configs.get(i));
System.out.println("Configuration " +
configs.get(i));
for (int j=0; j<cdata.size();
j++) {
PropertySetting s = cdata.getSetting(j);
System.out.println(" " +
s.getDeviceLabel() + ", " + s.getPropertyName() + ", " +
s.getPropertyValue());
}
}
Note the two new classes defined in the MMCoreJ: Configuration and
PropertySetting. PropertySetting is a triplet of strings: DeviceLabel,
PropertyName and PropertyValue. Configuration is just a collection of
PropertySetting objects.
To discover which configuration the system is currently in:
String config = core.getConfiguration();
This command can return an empty string if the system current state
does not match any of the defined configurations. getConfiguration
command will return the matching configuration name (if any) even if
the individual commands were used instead setConfiguration.
Synchronization
Each device loaded in MMCore reports its status by using "Busy" flag. A
device declares that it is busy if it is still executing the previous
command. To check the status of a single device or the entire system:
// check Z stage status
boolean ZStageBusy = core.deviceBusy("Z");
// check if any of the devices in the systema are busy
boolean systemBusy = core.systemBusy();
Very often you check the device status because you shouldn't proceed
with some action until the devices stopped moving or executing previous
commands. To relieve the porgrammer of the boring task of writing
polling loops, we provided special commands to implement waiting for
devices:
// wait until Z stage stopped moving
core.waitForDevice("Z");
core.snapImage();
// move to new XY position
core.SetPosition("X", 1230);
core.setPosition("Y", 330);
// wait until the all devices in the system stopped moving
core.waitForSystem();
core.snapImage();
Imaging
To further streamline synchronization tasks you can define all devices
which must be non-busy before the image is acquired.
// The following devices must stop moving before the image is acquired
core.assignImageSynchro("X");
core.assignImageSynchro("Y");
core.assignImageSynchro("Z");
core.assignImageSynchro("Emission");
// Set all the positions. For some of the devices it will take a while
// to stop moving
core.SetPosition("X", 1230);
core.setPosition("Y", 330);
core.SetPosition("Z", 8000);
core.setState("Emission", 3);
// Just go ahead and snap image. The system will automatically wait
// for all of the above devices to stop moving before the
// image is acquired
core.snapImage();
Shutter control
If the shutter is associated with the camera it usually needs to be
opened before an image is taken and closed as soon as acquisition is
done. If the "auto shutter" feature is turned on the system will
automatically perform these operations in the snapImage command.
// take image with auto shutter
core.setAutoShutter(true);
core.snapImage();
// take image with manual shutter
core.setAutoShutter(false); // disable auto shutter
core.setProperty("Shutter", "State", "1"); // open
core.waitForDevice("Shutter");
core.snapImage();
core.setProperty("Shutter", "State", "0"); // close
Configuring the system
As you have seen in the earlier sections at startup MMCore object
doesn't know about any devices, there are no labels defined, no options
set and no synchronization objects assigned. Before normal use of the
software the entire system needs to be configured according to the
current hardware setup. Here is an example configuration program to be
executed at system startup:
// load devices
core.loadDevice("Camera", "DemoCamera", "DCam");
core.loadDevice("Emission", "DemoCamera", "DWheel");
core.loadDevice("Excitation", "DemoCamera", "DWheel");
core.loadDevice("Dichroic", "DemoCamera", "DWheel");
core.loadDevice("Objective", "DemoCamera", "DObjective");
core.loadDevice("X", "DemoCamera", "DStage");
core.loadDevice("Y", "DemoCamera", "DStage");
core.loadDevice("Z", "DemoCamera", "DStage");
core.initializeAllDevices();
// Set labels for state devices
//
// emission filter
core.defineStateLabel("Emission", 0, "Chroma-D460");
core.defineStateLabel("Emission", 1, "Chroma-HQ620");
core.defineStateLabel("Emission", 2, "Chroma-HQ535");
core.defineStateLabel("Emission", 3, "Chroma-HQ700");
// excitation filter
core.defineStateLabel("Excitation", 2, "Chroma-D360");
core.defineStateLabel("Excitation", 3, "Chroma-HQ480");
core.defineStateLabel("Excitation", 4, "Chroma-HQ570");
core.defineStateLabel("Excitation", 5, "Chroma-HQ620");
// excitation dichroic
core.defineStateLabel("Dichroic", 0, "400DCLP");
core.defineStateLabel("Dichroic", 1, "Q505LP");
core.defineStateLabel("Dichroic", 2, "Q585LP");
// objective
core.defineStateLabel("Objective", 1, "Nikon 10X S Fluor");
core.defineStateLabel("Objective", 3, "Nikon 20X Plan Fluor ELWD");
core.defineStateLabel("Objective", 5, "Zeiss 4X Plan Apo");
// define configurations
//
core.defineConfiguration("FITC", "Emission", "State", "2");
core.defineConfiguration("FITC", "Excitation", "State", "3");
core.defineConfiguration("FITC", "Dichroic", "State", "1");
core.defineConfiguration("DAPI", "Emission", "State", "1");
core.defineConfiguration("DAPI", "Excitation", "State", "2");
core.defineConfiguration("DAPI", "Dichroic", "State", "0");
core.defineConfiguration("Rhodamine", "Emission", "State", "3");
core.defineConfiguration("Rhodamine", "Excitation", "State", "4");
core.defineConfiguration("Rhodamine", "Dichroic", "State", "2");
// set initial imaging mode
//
core.setProperty("Camera", "Exposure", "55");
core.setProperty("Objective", "Label", "Nikon 10X S Fluor");
core.setConfiguration("DAPI");
Complete example: Tutorial4.java
Configuration file
Instead of executing the initialization script or a program to load
devices, create configurations and perform other setup tasks each time
the system
starts, you can create equivalent configuration file which can be
executed with a single command:
core.loadSystemConfiguration("MMConfig.cfg");
In this case MMConfig.cfg is a text file in containing a list of simple
commands to
build the desired system state: devices, labels, equipment, properties,
and configurations. The format of this file and other topics related to
configuring the system are covered in Configuration
Guide.
The current system configuration can be saved by the complementary
saveConfiguration() command. For example you can build-up the system by
using script commands described in this Guide and when you verify that
everything is working, you can save the configuration in the file so
that it can be recalled in the future with the single command.
|