Micro-Manager Programming Guide
Created on November 16, 2005 by
Nenad Amodaj
Updated for release 1.0.37(beta) on July 28, 2006
Introduction
Micro-Manager central component is the MMCore class
encapsulating the entire automated microscope. MMCore
API represents a high-level abstraction for any combination of standard
components used in automated digital microscopy. A program written to
the MMCore
API should be able
to successfully execute regardless of specific devices used, or in the
worst case gracefully degrade if the minimal conditions are not
satisfied. By using run-time configuration discovery features of the MMCore
API, a program can automatically determine whether minimal conditions
for its execution are satisfied, e.g. if required devices are present
and if they have required capabilities.
MMCore API
enables the program
designer to build user interface or automation protocol in a
device-independent way and with minimal effort. MMCore
provides implementations for the most of the common tasks that
automated microscope is expected to perform in the laboratory or
screening setting.
We are using “program” term in a relatively loose
sense, since MMCore
implementation is in essence language agnostic. For example,
“program”
can be a C++ source code implementing complicated user interface or a
simple and small Beanshell (Java-like) script to acquire a sequence of
images. MMCore
API is designed with scripting transparency in mind. For purely
practical reasons all examples in this guide are shown in Java
programming language while API reference is specified in C++, but we
assume that translation of both to any other programming environment is
straightforward.
Setting up the run-time environment
MicroManage is in principle both platform independent and language
neutral. But, in practice MMCore
component is intended to run only on Windows, Mac OS X and Linux, and
to be used from C++, Java, Matlab and Python programs. Setting up the
environment is somewhat different for each language/OS combination.
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 .
Getting started
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.
Creating the 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.
Loading devices
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.
|