Background
My LIGO project lead to a job offer from LSU's Digital Media Center, to aid in creating content for interactive posters. To better expose high school students to the x-ray interferometry research conducted at LSU and useful tools, my role was to integrate ImageJ (an open source image processing and analysis tool) into the poster. Students would be able to interact with 3d models in the form of TIFF files related to the research.
A 4k widescreen monitor displayed all digital content, but accepted touch input wirelessly from a specialized Python application running on a Windows tablet. Through wifi, the tablet sends OSC messages (Open Sound Control, a protocol for networking multimedia devices, traditionally sound equipment) to the computer hosting the poster. However, ImageJ has no out-of-the-box capability to listen to these messages and needed modification.
ImageJOSC
An OSC-capable ImageJ tailored specifically for this poster was possible by integrating the Java osc library directly into ImageJ's source code.Through the tablet, students needed to be able to switch between different images, then explore the image in three dimensions by moving a cursor. To accomplish this in two dimensions, ImageJ's orthogonal views are used for YZ and XZ views, as shown to the right.
To integrate with the system already in place, the program reads YAML files from the main project directory. They provide static configuration information, such as the position that the main image window should appear when an TIFF file is opened, the directory where the TIFF files are located, the desired width and height of the window, and the format of expected OSC messages. A script runs this modified version of ImageJ in the background immediately when the poster launches, and from there it listens for a message requesting a specific image to open in the foreground. Once this occurs, another image may be opened to take its place, the image may be closed (if leaving the ImageJ section of the poster), or the orthogonal views cursor can be incremented to adjust the view of the model.
import ij.*;
import ij.plugin.Orthogonal_Views;
import com.illposed.osc.*; //OSC library
import ij.gui.ImageWindow;
import java.io.FileReader;
import java.util.*;
import java.util.logging.*;
import net.sourceforge.yamlbeans.YamlReader;
public class ImageJOSC {
public static final String EXAMP_POSTER_PATH = System.getProperty("user.dir") + "/config/exampPoster.yaml";
public static final String CONF_IMGJ_PATH = System.getProperty("user.dir") + "/config/config_size_position/confImgj.yaml";
public static final String OPTIONS_PATH = System.getProperty("user.dir") + "/src/_lupa/_imagej/config/options.yaml";
public final static Logger LOGGER=Logger.getLogger(ImageJOSC.class.getName());
public int receiverPort, stackSize, cursorIncrement;
public int xCursor=0,yCursor=0,zCursor=0;
public ImagePlus image;
public static String tiffPath;
public static Map optList;
public static Map addresses;
public boolean oscVisible = false;
public static void main(String[] args) throws Exception {
new ImageJOSC();
}
public ImageJOSC() throws Exception{
ImageJ ij=new ImageJ();
ij.setVisible(false);
loadConfig();
addOSCReceiver();
}
private void loadConfig() throws Exception {
//Loads all three yaml files
Map exampPoster = (Map) new YamlReader(new FileReader(EXAMP_POSTER_PATH)).read();
Map confImgj = (Map) new YamlReader(new FileReader(CONF_IMGJ_PATH)).read();
Map options = (Map) new YamlReader(new FileReader(OPTIONS_PATH)).read();
//Determine profile number
String profileNumber="1";
Map aspects = (Map)exampPoster.get("aspects");
for (Map.Entry entry : aspects.entrySet()) {
Map obj = entry.getValue();
if (obj.get("kind").equals("imgj"))
profileNumber = (String)obj.get("profile");
}
optList = (Map)((Map)confImgj.get(profileNumber)).get("opt_list"); //Read options list
tiffPath = (String)((Map)confImgj.get("meta")).get("filepath"); //Read path to all tiff files
cursorIncrement = Integer.parseInt((String)options.get("cursor_increment"));
Map osc = (Map)options.get("osc");
receiverPort = Integer.parseInt((String)osc.get("receiver_port"));
addresses = (Map)osc.get("addresses"); //Read the OSC addresses
}
private void addOSCReceiver() {
try {
OSCPortIn receiver= new OSCPortIn(receiverPort);
//Iterates through each address and adds it to a reciever, with a callback to oscMessageRecieved
for (Object address : addresses.values()) {
receiver.addListener((String)address, new OSCListener() {
@Override public void acceptMessage(java.util.Date time, OSCMessage message) {
oscMessageRecieved(message.getAddress(), message.getArguments());
}
});
}
receiver.startListening(); //Starts the listener
} catch (Exception e) {
LOGGER.log(Level.SEVERE, null, e);
}
}
//OSC message recieved
private void oscMessageRecieved(String address, Object[] args) {
//If a request has been recieved and no image is visible
if (!oscVisible && address.equals(addresses.get("load"))) {
Map imageInfo = (Map)optList.get((String)args[0]).get("imagej"); //Get image info
stackSize = Integer.parseInt((String)imageInfo.get("stack_size"));
//Parses size/location information from image info
Map size = (Map)imageInfo.get("size");
Map position = (Map)imageInfo.get("position");
int windowWidth = Integer.parseInt((String)size.get("size_x"));
int windowHeight = Integer.parseInt((String)size.get("size_y"));
int windowX = Integer.parseInt((String)position.get("pos_x"));
int windowY = Integer.parseInt((String)position.get("pos_y"));
//Shows the image
if (image!=null) image.close();
String imagePath = System.getProperty("user.dir") + "/" + tiffPath + imageInfo.get("image");
image=IJ.openImage(imagePath);
image.show();
//Set size/location from the information parsed earlier
ImageWindow frame = image.getWindow();
frame.setLocationAndSize(windowX, windowY, windowWidth, windowHeight);
frame.toFront();
//Open orthogonal views windows and reset the cursor to the center
xCursor=image.getWidth()/2;
yCursor=image.getHeight()/2;
zCursor=1;
IJ.runMacro("run(\"Orthogonal Views\")");
Orthogonal_Views.getInstance().setCrossLoc(xCursor,yCursor,zCursor);
oscVisible = true;
}
//If the window is in the foreground and the request is to move the cursor, move the cursor
if (oscVisible) {
if (address.equals(addresses.get("xy_right"))) {
xCursor+=cursorIncrement;
if (xCursor>image.getWidth()-1) xCursor=image.getWidth()-1; //Checks if the cursor is on the last pixel of the image and moves it no further
}
else if (address.equals(addresses.get("xy_left"))) {
xCursor-=cursorIncrement;
if (xCursor<0) xCursor=0;
}
else if (address.equals(addresses.get("xy_down"))) {
yCursor+=cursorIncrement;
if (yCursor>image.getHeight()-1) yCursor=image.getHeight()-1;
}
else if (address.equals(addresses.get("xy_up"))) {
yCursor-=cursorIncrement;
if (yCursor<0) yCursor=0;
}
else if (address.equals(addresses.get("z_right"))) {
zCursor+=cursorIncrement;
if (zCursor>stackSize) zCursor=stackSize;
}
else if (address.equals(addresses.get("z_left"))) {
zCursor-=cursorIncrement;
if (zCursor<1) zCursor=1;
}
Orthogonal_Views.getInstance().setCrossLoc(xCursor,yCursor,zCursor);
}
//Closes the image if requested
if (oscVisible && address.equals(addresses.get("hide"))) {
image.close();
oscVisible = false;
}
}
}