top bar

Micro-Manager Programming Guide

Revision as of 22:23, 29 September 2016 by Maximew (Talk | contribs)

(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)

Micro-Manager Programming Guide

Created on November 16, 2005 by Nenad Amodaj
Updated for release 1.0.37(beta) on July 28, 2006

Introduction

Before diving into Programming within Micro-Manager, it is useful to review the general architecture.

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.


The 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 the term "program" in a relatively loose sense, since MMCore implementation is in essence language agnostic. For example, a "program" can be C++ source code implementing complicated user interface or a simple and small Beanshell (Java-like) script to acquire a sequence of images. The 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

Micro-Manager 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.

See also: Search Paths

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. Instructions to work with several Java IDEs are available:


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 a 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 [content/code_examples/Tutorial1.java Tutorial1.java] and [content/code_examples/Tutorial2.java 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, the 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, the property mechanism allows us to control many details which are very specific to the device type. For example, some cameras will have an "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. The 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.
Note
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 system 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 programmer of the boring task of writing polling loops, we provided special commands to implement waiting for devices:

// wait until Z stage stops 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 stop 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 an 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.

© Micro-Manager : Vale Lab, UCSF 2006-2011 | All Rights Reserved | Contact