There are two components:
So here is how you can write a reader/writer yourself for your own custom image file format:
1 - The reader: write a plugin that (1) extends ImagePlus, (2) reads the file path passed as argument to the run method, or asks for one, and (3) places all the image data in the proper place -a FileInfo object contained within the ImagePlus:
public class Open_Custom_File_Format extends ImagePlus implements PlugIn {
public void run(String arg) {
String path = getPath(arg);
if (null == path) return;
if (!parse(path)) return;
if (null == arg || 0 == arg.trim().length()) this.show(); // was opened by direct call to the plugin
// not via HandleExtraFileTypes which would
// have given a non-null arg.
}
/** Accepts URLs as well. */
private String getPath(String arg) {
if (null != arg) {
if (0 == arg.indexOf("http://")
|| new File(arg).exists()) return arg;
}
// else, ask:
OpenDialog od = new OpenDialog("Choose a .mrc file", null);
String dir = od.getDirectory();
if (null == dir) return null; // dialog was canceled
dir = dir.replace('\\', '/'); // Windows safe
if (!dir.endsWith("/")) dir += "/";
return dir + od.getFileName();
}
/** Opens URLs as well. */
private InputStream open(String path) throws Exception {
if (0 == path.indexOf("http://"))
return new java.net.URL(path).openStream();
return new FileInputStream(path);
}
private boolean parse(String path) {
// Open file and read header
byte[] buf = new byte[136];
try {
InputStream stream = open(path);
is.read(buf, 0, 136);
is.close();
} catch (Exception e) {
e.printStackTrace();
return false;
}
// Read width,height,slices ... from the header
/* THIS IS AN EXAMPLE */
int width = readIntLittleEndian(buf, 0);
int height = readIntLittleEndian(buf, 4);
int n_slices = readIntLittleEndian(buf, 8);
int type = readIntLittleEndian(buf, 12);
// Build a new FileInfo object with all file format parameters and file data
FileInfo fi = new FileInfo();
fi.fileType = type;
fi.fileFormat = fi.RAW;
int islash = path.lastIndexOf('/');
if (0 == path.indexOf("http://")) {
fi.url = path;
} else {
fi.directory = path.substring(0, islash+1);
}
fi.fileName = path.substring(islash+1);
fi.width = width;
fi.height = height;
fi.nImages = slices;
fi.gapBetweenImages = 0;
fi.intelByteOrder = true; // little endian
fi.whiteIsZero = false; // no inverted LUT
fi.longOffset = fi.offset = 512; // header size, in bytes
// Now make a new ImagePlus out of the FileInfo
// and integrate its data into this PlugIn, which is also an ImagePlus
try {
FileOpener fo = new FileOpener(fi);
ImagePlus imp = fo.open(false);
this.setStack(imp.getTitle(), imp.getStack());
this.setCalibration(imp.getCalibration());
Object obinfo = imp.getProperty("Info");
if (null != obinfo) this.setProperty("Info", obinfo);
this.setFileInfo(imp.getOriginalFileInfo());
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
private final int readIntLittleEndian(byte[] buf, int start) {
return (buf[start]) + (buf[start+1]<<8) + (buf[start+2]<<16) + (buf[start+3]<<24);
}
}
Now you need only edit the HandleExtraFileTypes.java file, find the end of the list of custom file readers, and add your own:
...
if (name.endsWith(".myown")) {
return tryPlugIn("Open_Custom_File_Format", path);
}
...
You will need to recompile the HandleExtraFileTypes.java file, and of course your own custom plugin. Remember to place the plugin .java and .class files in the plugins folder, for example under Input-Output subfolder.
Examples:
2 - The writer: a plugin that gets the current image, or one passed by argument, and saves it to a user-defined file path:
public class Save_Custom_File_Format implements PlugIn {
public void run(String arg) {
ImagePlus imp = WindowManager.getCurrentImage();
if (null == imp) return;
SaveDialog sd = new SaveDialog("Save custom", "untitled", null);
String dir = sd.getDirectory();
if (null == dir) return; // user canceled dialog
dir = dir.replace('\\', '/'); // Windows safe
if (!dir.endsWith("/")) dir += "/";
saveCustom(imp, dir + sd.getFileName());
}
static public void saveCustom(ImagePlus imp, String path) {
File file = new File(path);
DataOutputStream dos = null;
try {
dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file)));
// read data:
FileInfo fi = imp.getFileInfo();
// HEADER: ... read all header tags and metadata
dos.write( ... );
// BODY: ... read all stack slices (or single slice)
for (int i=1; i<imp.getNSlices(); i++) {
dos.write( ... );
}
dos.flush();
} catch (Exception e) {
e.printStackTrace();
} finally {
dos.close();
}
}
}
Note how above I have created a separate, static public method to save the image. In this fashion, ImageJ can save your file from the menus, but also any of your other plugins can save the file without having to invoke the plugin (and thus Thread-safe).
To make ImageJ aware of your file writer in the "File / Save As" menu, write a text file titled "plugins.config" with the contents similar to below:
# Name: Custom File Writer # Author: Yourself # Date: 2008/04/12 File>Save As, "My Custom Writer...", Save_Custom_File_Format
Finally, make a jar file with the Save_Custom_File_Format.java, Save_Custom_File_Format.class and plugins.config files. In a terminal, type:
$ jar cf My_Custom_Writer.jar Save_Custom_File_Format* plugins.config
Final notes:
Multithreading means running processes in parallel. Only completely independent tasks can be run in parallel. What makes sense to multithread?
The good news is that ImageJ will process stack slices in parallel automatically, at least as of version 1.40 and later.
As for processing multiple images in parallel: ImageJ 1.41f and later lets you apply any sequence of plugins (and commands, which are/behave like plugins) to a given image in a thread-safe manner.
The core idea: the concept of current image over which any invoked plugins are applied has now been reduced to the current image of the current thread that runs the plugin. The same encapsulation is provided for Macro options, which provide parameter values for plugins and automatically fill in text fields, checkboxes and pull down menus in dialogs.
The new changes in ij 1.41f are:
Armed with the new ij 1.41f functionality, we can finally safely program in a multithreaded way at the high level of invoking commands in an image.
For example: test multiple parameters for segmentation of the dots in the dot blot sample image.
import ij.*;
import ij.plugin.PlugIn;
import ij.process.*;
import ij.io.Opener;
import java.util.concurrent.atomic.AtomicInteger;
public class A_Parallel_High_Level_Plugin implements PlugIn {
public void run(String arg) {
final ImagePlus dot_blot = new Opener().openURL("http://rsb.info.nih.gov/ij/images/Dot_Blot.jpg");
final int starting_threshold = 190;
final int ending_threshold = 255;
final int n_tests = ending_threshold - starting_threshold + 1;
final AtomicInteger ai = new AtomicInteger(starting_threshold);
// store all result images here
final ImageProcessor[] results = new ImageProcessor[n_tests];
final Thread[] threads = newThreadArray();
for (int ithread = 0; ithread < threads.length; ithread++) {
// Concurrently run in as many threads as CPUs
threads[ithread] = new Thread() {
public void run() {
// Each thread processes a few items in the total list
// Each loop iteration within the run method has a unique 'i' number to work with
// and to use as index in the results array:
for (int i = ai.getAndIncrement(); i <= ending_threshold; i = ai.getAndIncrement()) {
// 'i' is the lower bound of the threshold window
ImageProcessor ip = dot_blot.getProcessor().duplicate();
ip.setMinAndMax(i, 255);
ImagePlus imp = new ImagePlus("Threshold " + i, ip);
// Run the plugins on the new image:
IJ.run(imp, "Convert to Mask", "");
IJ.run(imp, "Analyze Particles...", "size=800-20000 circularity=0.00-1.00 show=Outlines");
// The above results in a newly opened image, with the unique name "Drawing of Threshold " + i
// cleanup:
imp.flush();
// Capture and store resulting image (WindowManager.getImage is a synchronized, thread-safe method)
ImagePlus res = WindowManager.getImage("Drawing of Threshold " + i);
results[i] = res.getProcessor();
res.getWindow().setVisible(false);
}
}
};
}
startAndJoin(threads);
// now the results array is full. Just show them in a stack:
final ImageStack stack = new ImageStack(dot_blot.getWidth(), dot_blot.getHeight());
for (int i=0; i< results.length; i++) {
stack.addSlice(Integer.toString(i), results[i]);
}
new ImagePlus("Results", stack).show();
}
/** Create a Thread[] array as large as the number of processors available.
* From Stephan Preibisch's Multithreading.java class. See:
* http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
*/
private Thread[] newThreadArray() {
int n_cpus = Runtime.getRuntime().availableProcessors();
return new Thread[n_cpus];
}
/** Start all given threads and wait on each of them until all are done.
* From Stephan Preibisch's Multithreading.java class. See:
* http://repo.or.cz/w/trakem2.git?a=blob;f=mpi/fruitfly/general/MultiThreading.java;hb=HEAD
*/
public static void startAndJoin(Thread[] threads)
{
for (int ithread = 0; ithread < threads.length; ++ithread)
{
threads[ithread].setPriority(Thread.NORM_PRIORITY);
threads[ithread].start();
}
try
{
for (int ithread = 0; ithread < threads.length; ++ithread)
threads[ithread].join();
} catch (InterruptedException ie)
{
throw new RuntimeException(ie);
}
}
}
An ImageJ image consists of three parts:
An ImageJ image stack consists of four parts, very similar to the above:
When using stacks, bear in mind the following:
The java.awt.Image contained in the ImagePlus is re-created new when calling updateAndDraw() on the ImagePlus. You must call updateAndDraw() after modifying the pixels if you want the change to be reflected on an image currently being displayed on the screen.
int width = 400; int height = 400; ImageProcessor ip = new ByteProcessor(width, height); String title = "My new image"; ImagePlus imp = new ImagePlus(title, ip); imp.show();
There are several ImageProcessor classes, each with its own specialized set of constructors. See the ImageJ API at ImageProcessor (the parent abstract class of all processors), ByteProcessor, ShortProcessor, FloatProcessor, and ColorProcessor.
new ImagePlus("My new image", new ByteProcessor(400, 400)).show();
A simple 8-bit grayscale image of 400x400 pixels:
ImagePlus imp = IJ.createImage("My new image", "8-bit black", 400, 400, 1);
imp.show();
// or, without getting back a reference:
IJ.newImage("My new image", "8-bit black", 400, 400, 1);
A stack of 10 color images of 400x400 pixels:
ImagePlus imp = IJ.createImage("My new image", "RGB white", 400, 400, 10);
imp.show();
// again, without getting back a reference:
IJ.newImage("My new image", "RGB white", 400, 400, 10);
A call to flush() will release all memory resources used by the ImagePlus.
ImagePlus imp = ... imp.flush();
WARNING! If you are holding a pointer to the ImageProcessor as obtained from ImagePlus getProcessor() method, the pixel array pointer of that ImageProcessor will be set to null. You should instead call duplicate() on the ImageProcessor, or get its pixels directly via getPixels() and store them in a new ImageProcessor of the same dimensions (setting along the LUT, etc).
Likewise, the java.awt.Image gets its own method flush() invoked as well.
All methods revolve around the ij.io.Opener class.
ImagePlus imp = IJ.openImage("/path/to/image.tif");
imp.show();
ImagePlus imp = IJ.openImage("http://www.example.org/path/to/image.tif");
imp.show();
// Without getting back a pointer, and automatically showing it:
IJ.open("/path/to/image.tif");
// Same but from an URL
IJ.open("http://www.example.org/path/to/image.tif");
Thanks to Wayne Rasband for the above suggestions.
Opener opener = new Opener();
ImagePlus imp = opener.openImage("/path/to/image.tif");
imp.show();
Opener opener = new Opener();
ImagePlus imp = opener.openImage("http://www.example.org/path/to/image.tif");
imp.show();
Above notice how the URL http:// is automatically detected and properly parsed. If desired, one can directly call:
...
ImagePlus imp = opener.openURL("http://www.example.org/path/to/image.tif");
...
In a high-level way, pixels may be edited by calling ImageJ commands on an image:
ImagePlus imp = ... // Making a binary image IJ.run(imp, "Convert to Mask", ""); // "" means no arguments // Resizing, opens a copy in a new window (the 'create' command keyword) IJ.run(imp, "Scale...", "x=0.5 y=0.5 width=344 height=345 interpolate create title=[Scaled version of " + imp.getTitle() + "]"); ...
Any ImageJ command may be applied. You can find out which commands to use and which arguments to give them by running the Plugins - Macros - Recorder, and manually calling ImageJ menu commands on an open image.
To draw or fill the ROI (Region Of Interest) on an image:
ImagePlus imp = ...
ImageProcessor ip = imp.getProcessor();
// Assuming 8-bit image
// fill a rectangular region with 255 (on grayscale this is white color):
Roi roi = new Roi(30, 40, 100, 100); // x, y, width, height of the rectangle
ip.setRoi(roi);
ip.setValue(255);
ip.fill();
// fill an oval region with 255 (white color when grayscale LUT):
OvalRoi oroi = new OvalRoi(50, 60, 100, 150); // x, y, width, height of the oval
ip.setRoi(oroi);
ip.setValue(255);
ip.fill(ip.getMask()); // notice different fill method
// regular fill() would fill the entire bounding box rectangle of the OvalRoi
// The method above is valid at least for PolygonRoi and ShapeRoi as well.
// draw the contour of any region with 255 pixel intensity
Roi roi = ...
ip.setValue(255);
ip.draw();
// update screen view of the image
imp.updateAndDraw();
A few words on ROIs:
There are many selection/ROI types: Roi (the rectangular one, and also parent of all others), Line, OvalRoi, PolygonRoi, PointRoi, FreehandRoi, ShapeRoi, TextRoi. In addition some have subtypes, such as the POLYGON and POLYLINE types for PolygonRoi.
Most ROIs are useful for editing images; a few for image analysis (Line, PointRoi, TextRoi).
The most powerful ROI is the ShapeRoi: backed up by a java.awt.geom.GeneralPath, it's capable of storing any number of disconnected regions of interest of any shape.
The ip.fill(ip.getMask()) method is safest to use in all occasions, needing only a check for whether the ImageProcessor mask returned by getMask() is not null.
To rotate, flip and scale the image (or only its ROI, if any):
ImagePlus imp = ... ImageProcessor ip = imp.getProcessor(); ip.flipHorizontal(); ip.flipVertical(); ip.rotateLeft(); ip.rotateRight(); // rotate WITHOUT enlarging the canvas to fit double angle = 45; ip.setInterpolate(true); // bilinear ip.rotate(45); // rotate ENLARGING the canvas and filling the new areas with background color double angle = 45; IJ.run(imp, "Arbitrarily...", "angle=" + angle + " grid=1 interpolate enlarge"); // scale WITHOUT modifying the canvas dimensions ip.setInterpolate(true); // bilinear ip.scale(2.0, 2.0); // in X and Y // scale ENLARGING or SHRINKING the canvas dimensions double sx = 2.0; double sy = 0.75; int new_width = (int)(ip.getWidth() * sx); int new_height = (int)(ip.getHeight() * sy); ip.setInterpolate(true); // bilinear ImageProcesor ip2 = ip.resize(new_width, new_height); // of the same type as the original imp.setProcessor(imp.getTitle(), ip2); // UPDATE the original ImagePlus // update screen view of the image imp.updateAndDraw();
The ImageProcessor class offers methods to draw lines, text and dots, and many more. Have a look at the ImageProcessor API.
ImagePlus imp = ...
ImageProcessor ip = imp.getProcessor();
// Editing the pixel array
if (imp.getType() == ImagePlus.GRAY8) {
byte[] pixels = (byte[])ip.getPixels();
// ... do whatever operations directly on the pixel array
}
// Replacing the pixel array: ONLY if same size
if (imp.getType() == ImagePlus.GRAY8) {
int width = ip.getWidth();
int height = ip.getHeight();
byte[] new_pixels = new byte[width * height];
// set each pixel value to whatever, between -128 and 127
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
// Editing pixel at x,y position
new_pixels[y * width + x] = ...;
}
}
// update ImageProcessor to new array
ip.setPixels(new_pixels);
}
// Replacing the pixel array but of different length: for example, to resize 2.5 times in width and height
int new_width = (int)(ip.getWidth() * 2.5);
int new_height = (int)(ip.getHeight() * 2.5);
ImageProcessor ip2 = ip.createProcessor(new_width, new_height); // of same type
imp.setProcessor(imp.getTitle(), ip2);
if (imp.getType() == ImagePlus.GRAY8) {
byte[] pix = (byte[])imp.getProcessor().getPixels(); // or ip2.getPixels();
// .. process pixels ...
for (int y=0; y<height; y++) {
for (int x=0; x<width; x++) {
// Editing pixel at x,y position
new_pixels[y * width + x] = ...;
}
}
}
// DON'T forget to update the screen image!
imp.updateAndDraw();
Of course updating the screen image is only necessary if you are displaying the ImagePlus.
ImagePlus imp = ... IJ.saveAs(imp, "tif", "/path/to/image.tif"); // or by using the file format extension: IJ.save(imp, "/path/to/image.tif");
Many formats are supported. Search for method "saveAs" in class IJ.
ImagePlus imp = ...
new FileSaver(imp).saveAsTiff("/path/to/image.tif");
The FileSaver class has many more options: saveAsTiffStack, saveAsJpeg, saveAsPng, saveAsGif ... etc.
That is, how to fill in automatically the values in a dialog, so that it never shows up. In this way, plugins that require dialogs can be used for batch processing without user intervention.
The key points:
An example in javascript:
importClass(Packages.java.lang.Thread);
importClass(Packages.ij.Macro);
// Prepare options for Bandpass filter
options = "filter_large=40 filter_small=3 suppress=None tolerance=5 autoscale saturate";
// Get the current thread
thread = Thread.currentThread();
original_name = thread.getName();
// Rename current thread
thread.setName("Run$_my_batch_process");
// Set the options for the current thread
Macro.setOptions(Thread.currentThread(), options);
// Get the current image
imp = IJ.getImage();
// Finally, run the bandpass filter without dialogs:
IJ.runPlugIn(imp, "ij.plugin.filter.FFTFilter", "");
// Be nice: undo naming, so other scripts may run with dialogs
thread.setName(original_name);
// Be safe: remove the thread's options from the table
// (which also removes the reference to the thread itself)
Macro.setOptions(thread, null);
To test the above code, just go to menu "PlugIns - New - Javascript" and paste it in the text window that opens. The push control+j, or choose "Macros - Evaluate Javascript".
A detailed example, in java:
/* An example plugin to illustrate the automation of GenericDialog
* i.e. how to call a function that would normally show a dialog
* with options, with those options already filled in automatically.
*
* The process consists in:
* 1 - Renaming the current thread to "Run$_" + any name.
* 2 - Setting the Macro options String for that thread.
* 3 - Calling the function normally, but now the dialog never shows
* and its options are automatically filled in from the Macro options.
*
* Notice that:
*
* 1 - The text label of the option becomes a key in the table of options.
*
* 2 - Options with an underscore in their name will show it in the dialog as
* a blank space, but still the option name itself (the key) has the
* underscore. This enables options like 'the_width' and 'the_height' to be
* different (as opposed to the value set for 'the' being given to both).
*
* 3 - Boolean options, a.k.a. checkboxes, are true when present and false
* otherwise.
*
* 4 - Choices, a.k.a. pulldown menus, need the exact String value desired,
* not an index.
*
* 5 - If a key is absent in the Macro options string, its default value is
* taken. The default value is the one that appears in the dialog when
* opened.
*
* 6 - Text values are set with single quotes: title='this and that'
*/
import ij.plugin.PlugIn;
import ij.gui.GenericDialog;
import ij.ImagePlus;
import ij.process.*;
import ij.IJ;
import ij.Macro;
import java.awt.Color;
public class Create_image_from_dialog implements PlugIn {
public void run(String arg) {
// A - Without automation: the dialog shows
ImagePlus imp = createImage();
if (null != imp) imp.show();
// B - With automation: the dialog never shows:
Thread thread = Thread.currentThread();
thread.setName("Run$_create_image");
// ... so we can create many images in a loop, for example.
for (int i=1; i<=3; i++) {
// Create a 1024x1024 image, 160bit, with noise added
// and with the title storing the loop index:
Macro.setOptions(thread, "title='My new image " + i + "'"
+ " width=1024 height=1024 type='16-bit' add_noise");
// Above, notice how we do not set the fill_value key,
// so that its default value (zero in this case) is taken.
ImagePlus imp2 = createImage();
imp2.show();
}
// Cleanup: remove reference to the Thread and its associated options
Macro.setOptions(thread, null);
}
public ImagePlus createImage() {
final GenericDialog gd = new GenericDialog("Create image");
gd.addStringField("title:", "new");
gd.addNumericField("width:", 512, 0);
gd.addNumericField("height:", 512, 0);
final String[] types = new String[]{"8-bit", "16-bit", "32-bit", "RGB"};
gd.addChoice("type:", types, types[0]);
gd.addSlider("fill_value:", 0, 255, 0); // min, max, default
gd.addCheckbox("add_noise", false);
gd.showDialog();
if (gd.wasCanceled()) return null;
final String title = gd.getNextString();
final int width = (int)gd.getNextNumber();
final int height = (int)gd.getNextNumber();
final int itype = gd.getNextChoiceIndex();
final double fill_value = gd.getNextNumber();
final boolean add_noise = gd.getNextBoolean();
ImageProcessor ip = null;
switch (itype) {
case 0: ip = new ByteProcessor(width, height); break;
case 1: ip = new ShortProcessor(width, height); break;
case 2: ip = new FloatProcessor(width, height); break;
case 3: ip = new ColorProcessor(width, height); break;
}
// Color images are created filled with white by default
if (3 == itype && 255 != fill_value) {
// color image
ip.setColor(new Color((int)fill_value, (int)fill_value, (int)fill_value));
ip.fill();
}
// non-color images
else if (0 != fill_value) {
ip.setValue(fill_value);
ip.fill();
}
final ImagePlus imp = new ImagePlus(title, ip);
if (add_noise) IJ.run(imp, "Add Noise", "");
return imp;
}
}