package edu.stanford.rsl.conrad.io;
// Nrrd_Reader
// -----------
// (c) Gregory Jefferis 2007
// Department of Zoology, University of Cambridge
// jefferis@gmail.com
// All rights reserved
// Source code released under Lesser Gnu Public License v2
// TODO
// - Support for multichannel images
// (problem is how to figure out they are multichannel in the absence of
// other info - not strictly required by nrrd format)
// - time datasets
// - line skip (only byte skip at present)
// - calculating spacing information from axis mins/cell info
// Compiling:
// You must compile Nrrd_Writer.java first because this plugin
// depends on the NrrdFileInfo class declared in that file
import ij.IJ;
import ij.ImagePlus;
import ij.io.FileInfo;
import ij.io.FileOpener;
import ij.io.OpenDialog;
import ij.measure.Calibration;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import edu.stanford.rsl.conrad.numerics.SimpleMatrix;
import edu.stanford.rsl.conrad.numerics.SimpleVector;
import edu.stanford.rsl.conrad.utils.CONRAD;
/**
* ImageJ plugin to read a file in Gordon Kindlmann's NRRD
* or 'nearly raw raster data' format, a simple format which handles
* coordinate systems and data types in a very general way.
* See <A HREF="http://teem.sourceforge.net/nrrd">http://teem.sourceforge.net/nrrd</A>
* and <A HREF="http://flybrain.stanford.edu/nrrd">http://flybrain.stanford.edu/nrrd</A>
*/
public class NrrdFileReader extends ImagePlus
{
public final String uint8Types="uchar, unsigned char, uint8, uint8_t";
public final String int16Types="short, short int, signed short, signed short int, int16, int16_t";
public final String uint16Types="ushort, unsigned short, unsigned short int, uint16, uint16_t";
public final String int32Types="int, signed int, int32, int32_t";
public final String uint32Types="uint, unsigned int, uint32, uint32_t";
private String notes = "";
private boolean detachedHeader=false;
public String headerPath=null;
public String imagePath=null;
public String imageName=null;
public void run(String arg) {
String directory = "", name = arg;
if ((arg==null) || (arg==""))
{
OpenDialog od = new OpenDialog("Load Nrrd (or .nhdr) File...", arg);
name = od.getFileName();
if (name==null)
return;
directory = od.getDirectory();
}
else
{
File dest = new File(arg);
directory = dest.getParent();
name = dest.getName();
}
ImagePlus imp = load(directory, name);
if (imp==null) return; // failed to load the file
if (imageName!=null) {
// set the name of the image to the name found inside the load method
// TOFIX - what should the name be? There could be several
// image files referenced in a single detached .nhdr
setStack(imageName, imp.getStack());
} else {
setStack(name, imp.getStack());
}
if (!notes.equals(""))
setProperty("Info", notes);
// bring over the calibration information as well
copyScale(imp);
// if we weren't sent a filename but chose one, then show the image
if (arg.equals("")) show();
}
public ImagePlus load(String directory, String fileName) {
if (!directory.endsWith(File.separator)) directory += File.separator;
if ((fileName == null) || (fileName == "")) return null;
// imagePath=fi.directory+fi.fileName;
// headerPath=fi.directory+fi.fileName;
NrrdFileInfo fi;
try {
fi=getHeaderInfo(directory, fileName);
}
catch (IOException e) {
CONRAD.log("readHeader: "+ e.getMessage());
return null;
}
if (IJ.debugMode) CONRAD.log("fi:"+fi);
IJ.showStatus("Loading Nrrd File: " + directory + fileName);
ImagePlus imp; FlexibleFileOpener gzfo;
if(fi.encoding.equals("gzip") && detachedHeader) {
// call my nice gzip opener plugin which has had the
// createInputStream method overloaded.
gzfo = new FlexibleFileOpener(fi,FlexibleFileOpener.GZIP);
imp = gzfo.open(false);
} else if(fi.encoding.equals("gzip")) {
long preOffset=fi.longOffset>0?fi.longOffset:fi.offset;
fi.offset=0;fi.longOffset=0;
gzfo= new FlexibleFileOpener(fi,FlexibleFileOpener.GZIP,preOffset);
if (IJ.debugMode) CONRAD.log("gzfo:"+gzfo);
imp = gzfo.open(false);
} else {
FileOpener fo = new FileOpener(fi);
imp = fo.open(false);
}
if(imp==null) return null;
// Copy over the spatial scale info which we found in readHeader
// nb the first we don't just overwrite the current calibration
// because this may have density calibration for signed images
Calibration cal = imp.getCalibration();
Calibration spatialCal = this.getCalibration();
cal.pixelWidth=spatialCal.pixelWidth;
cal.pixelHeight=spatialCal.pixelHeight;
cal.pixelDepth=spatialCal.pixelDepth;
cal.setUnit(spatialCal.getUnit());
cal.xOrigin=spatialCal.xOrigin;
cal.yOrigin=spatialCal.yOrigin;
cal.zOrigin=spatialCal.zOrigin;
imp.setCalibration(cal);
return imp;
}
public NrrdFileInfo getHeaderInfo( String directory, String fileName ) throws IOException {
if (IJ.debugMode) CONRAD.log("Entering Nrrd_Reader.readHeader():");
NrrdFileInfo fi = new NrrdFileInfo();
fi.directory=directory; fi.fileName=fileName;
Calibration spatialCal = this.getCalibration();
// NB Need RAF in order to ensure that we know file offset
RandomAccessFile input = new RandomAccessFile(fi.directory+fi.fileName,"r");
String thisLine,noteType,noteValue, noteValuelc;
fi.fileType = FileInfo.GRAY8; // just assume this for the mo
spatialCal.setUnit("micron"); // just assume this for the mo
fi.fileFormat = FileInfo.RAW;
fi.nImages = 1;
// parse the header file, until reach an empty line// boolean keepReading=true;
while(true) {
thisLine=input.readLine();
if(thisLine==null || thisLine.equals("")) {
if(!detachedHeader) fi.longOffset = input.getFilePointer();
break;
}
notes+=thisLine+"\n";
if(thisLine.indexOf("#")==0) continue; // ignore comments
noteType=getFieldPart(thisLine,0).toLowerCase(); // case irrelevant
noteValue=getFieldPart(thisLine,1);
noteValuelc=noteValue.toLowerCase();
String firstNoteValue=getSubField(thisLine,0);
// String firstNoteValuelc=firstNoteValue.toLowerCase();
if (IJ.debugMode) CONRAD.log("NoteType:"+noteType+", noteValue:"+noteValue);
if (noteType.equals("data file")||noteType.equals("datafile")) {
// This is a detached header file
// There are 3 kinds of specification for the data files
// 1. data file: <filename>
// 2. data file: <format> <min> <max> <step> [<subdim>]
// 3. data file: LIST [<subdim>]
if(firstNoteValue.equals("LIST")) {
// TOFIX - type 3
throw new IOException("Nrrd_Reader: not yet able to handle datafile: LIST specifications");
} else if(!getSubField(thisLine,1).equals("")) {
// TOFIX - type 2
throw new IOException("Nrrd_Reader: not yet able to handle datafile: sprintf file specifications");
} else {
// Type 1 specification
File imageFile;
// Relative or absolute
if(noteValue.indexOf("/")==0) {
// absolute
imageFile=new File(noteValue);
// TOFIX could also check local directory if absolute path given
// but dir does not exist
} else {
//CONRAD.log("fi.directory = "+fi.directory);
imageFile=new File(fi.directory,noteValue);
}
//CONRAD.log("image file ="+imageFile);
if(imageFile.exists()) {
fi.directory=imageFile.getParent();
fi.fileName=imageFile.getName();
imagePath=imageFile.getPath();
detachedHeader=true;
} else {
throw new IOException("Unable to find image file ="+imageFile.getPath());
}
}
}
if (noteType.equals("dimension")) {
fi.dimension=Integer.valueOf(noteValue).intValue();
if(fi.dimension>3) throw new IOException("Nrrd_Reader: Dimension>3 not yet implemented!");
}
if (noteType.equals("sizes")) {
fi.sizes=new int[fi.dimension];
for(int i=0;i<fi.dimension;i++) {
fi.sizes[i]=Integer.valueOf(getSubField(thisLine,i)).intValue();
if(i==0) fi.width=fi.sizes[0];
if(i==1) fi.height=fi.sizes[1];
if(i==2) fi.nImages=fi.sizes[2];
}
//System.out.println("Number of Images: " + fi.nImages);
}
if (noteType.equals("units")) spatialCal.setUnit(firstNoteValue);
if (noteType.equals("spacings")) {
double[] spacings=new double[fi.dimension];
for(int i=0;i<fi.dimension;i++) {
// TOFIX - this order of allocations is not a given!
spacings[i]=Double.valueOf(getSubField(thisLine,i)).doubleValue();
if(i==0) spatialCal.pixelWidth=spacings[0];
if(i==1) spatialCal.pixelHeight=spacings[1];
if(i==2) spatialCal.pixelDepth=spacings[2];
}
fi.spacing = spacings;
}
if (noteType.equals("centers") || noteType.equals("centerings")) {
fi.centers=new String[fi.dimension];
for(int i=0;i<fi.dimension;i++) {
// TOFIX - this order of allocations is not a given!
fi.centers[i]=getSubField(thisLine,i);
}
}
if (noteType.equals("type")) {
if (uint8Types.indexOf(noteValuelc)>=0) {
fi.fileType=FileInfo.GRAY8;
} else if(uint16Types.indexOf(noteValuelc)>=0) {
fi.fileType=FileInfo.GRAY16_SIGNED;
} else if(int16Types.indexOf(noteValuelc)>=0) {
fi.fileType=FileInfo.GRAY16_UNSIGNED;
} else if(uint32Types.indexOf(noteValuelc)>=0) {
fi.fileType=FileInfo.GRAY32_UNSIGNED;
} else if(int32Types.indexOf(noteValuelc)>=0) {
fi.fileType=FileInfo.GRAY32_INT;
} else if(noteValuelc.equals("float")) {
fi.fileType=FileInfo.GRAY32_FLOAT;
} else if(noteValuelc.equals("double")) {
fi.fileType=FileInfo.GRAY64_FLOAT;
} else {
throw new IOException("Unimplemented data type ="+noteValue);
}
}
if (noteType.equals("byte skip")||noteType.equals("byteskip")) fi.longOffset=Long.valueOf(noteValue).longValue();
if (noteType.equals("endian")) {
if(noteValuelc.equals("little")) {
fi.intelByteOrder = true;
} else {
fi.intelByteOrder = false;
}
}
if (noteType.equals("encoding")) {
if(noteValuelc.equals("gz")) noteValuelc="gzip";
fi.encoding=noteValuelc;
}
if(noteType.equals("space directions")){
SimpleMatrix dir = new SimpleMatrix(fi.dimension, fi.dimension);
for(int i = 0; i < fi.dimension; i++){
String sub = getSubField(thisLine,i);
dir.setColValue(i, parseVector(sub, fi.dimension));
}
fi.setSpaceDirections(dir);
// NRRD4 supports spacing information within the direction.
// Check if spacing was already set by the "spacing" case. If not, read it from space directions.
// If there is a spacings entry down the line, it will override this one.
if(fi.spacing == null) {
// Get spacing from direction vectors.
double[] spacings = new double[fi.dimension];
// Loop over columns.
for(int i = 0; i < fi.dimension; i++){
SimpleVector col = dir.getCol(i);
// The spacing is given as the norm of the direction vector.
spacings[i] = col.normL2();
// TOFIX - this order of allocations is not a given!
if(i==0) spatialCal.pixelWidth=spacings[0];
if(i==1) spatialCal.pixelHeight=spacings[1];
if(i==2) spatialCal.pixelDepth=spacings[2];
}
fi.spacing = spacings;
}
}
if(noteType.equals("space origin")){
fi.setSpaceOrigin(parseVector(getSubField(thisLine, 0), fi.dimension));
}
if (noteType.equals("axis mins") || noteType.equals("axismins")) {
double[] axismins=new double[fi.dimension];
for(int i=0;i<fi.dimension;i++) {
// TOFIX - this order of allocations is not a given!
// NB xOrigin are in pixels, whereas axismins are of course
// in units; these are converted later
axismins[i]=Double.valueOf(getSubField(thisLine,i)).doubleValue();
if(i==0) spatialCal.xOrigin=axismins[0];
if(i==1) spatialCal.yOrigin=axismins[1];
if(i==2) spatialCal.zOrigin=axismins[2];
}
fi.setSpaceOrigin(new SimpleVector(axismins));
}
}
// Fix axis mins, converting them to pixels
// if clause is to guard against cases where there is no spatial
// calibration info leading to Inf
if(spatialCal.pixelWidth!=0) spatialCal.xOrigin=-spatialCal.xOrigin/spatialCal.pixelWidth;
if(spatialCal.pixelHeight!=0) spatialCal.yOrigin=-spatialCal.yOrigin/spatialCal.pixelHeight;
if(spatialCal.pixelDepth!=0) spatialCal.zOrigin=-spatialCal.zOrigin/spatialCal.pixelDepth;
// Axis min will be the centre of the first pixel if this a "cell" nrrd
// or at the (top, front, left) if this is a "node" nrrd.
// ImageJ works on a node basis - that is it treats each voxel as a
// cube located at its top left corner (or more accurately I think the
// corner closer to the coordinate origin); however the image extent
// displayed is the "bounds" ie spacing*n
// So to convert a cell based nrrd to a node based ImagePlus, need to
// shift origin by 1/2 voxel dims in each dimension
// Since the nrrd specified origin would have been the centre of the
// voxel we need to SUBTRACT 1/2 voxel dims for ImageJ
// See http://teem.sourceforge.net/nrrd/format.html#centers
if(fi.centers!=null) {
if(fi.centers[0].equals("cell")) spatialCal.xOrigin-=spatialCal.pixelWidth/2;
if(fi.centers[1].equals("cell")) spatialCal.yOrigin-=spatialCal.pixelHeight/2;
if(fi.dimension>2 && fi.centers[2].equals("cell")) spatialCal.zOrigin-=spatialCal.pixelDepth/2;
}
if(!detachedHeader) fi.longOffset = input.getFilePointer();
input.close();
this.setCalibration(spatialCal);
return (fi);
}
// This gets a space delimited field from a nrrd string
// of the form
// a long name: space delimited values
// but note only works with Java >=1.4 Ithink
String getFieldPart(String str, int fieldIndex) {
str=str.trim(); // trim the string
String[] fieldParts=str.split(":\\s+");
if(fieldParts.length<2) return(fieldParts[0]);
//CONRAD.log("field = "+fieldParts[0]+"; value = "+fieldParts[1]+"; fieldIndex = "+fieldIndex);
if(fieldIndex==0) return fieldParts[0];
else return fieldParts[1];
}
String getSubField(String str, int fieldIndex) {
String fieldDescriptor=getFieldPart(str,1);
fieldDescriptor=fieldDescriptor.trim(); // trim the string
if (IJ.debugMode) CONRAD.log("fieldDescriptor = "+fieldDescriptor+"; fieldIndex = "+fieldIndex);
String[] fields_values=fieldDescriptor.split("\\s+");
if (fieldIndex>=fields_values.length) {
return "";
} else {
String rval=fields_values[fieldIndex];
if(rval.startsWith("\"")) rval=rval.substring(1);
if(rval.endsWith("\"")) rval=rval.substring(0, rval.length()-1);
return rval;
}
}
private SimpleVector parseVector(String s, int dim){
SimpleVector v = new SimpleVector(dim);
int idx1 = 0;
for(int i = 0; i < dim; i++){
int idx2;
if(i == dim-1){
idx2 = s.indexOf(")");
}else{
idx2 = s.indexOf(",", idx1+1);
}
v.setElementValue(i, Double.valueOf(s.substring(idx1+1, idx2)));
idx1 = idx2;
}
return v;
}
}