package com.srbenoit.microscopy;
import java.awt.image.BufferedImage;
import java.io.File;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.logging.Level;
import javax.imageio.ImageIO;
import javax.swing.JOptionPane;
import com.srbenoit.filter.AbstractFilter;
import com.srbenoit.filter.FilterException;
import com.srbenoit.filter.FilterOutput;
import com.srbenoit.filter.FilterTreeExecutor;
import com.srbenoit.filter.Pipe;
import com.srbenoit.filter.items.ImageArrayPipeItem;
import com.srbenoit.filter.items.StringPipeItem;
import com.srbenoit.filter.items.TimeSeriesPipeItem;
import com.srbenoit.util.LocalTime;
import loci.formats.ClassList;
import loci.formats.IFormatReader;
import loci.formats.ImageReader;
import loci.formats.in.MetamorphTiffReader;
/**
* A filter to scan a directory for a set of MetaMorph image series, prompt the user to select a
* series from those available, load the series files, then prompt the user to designate
* subsequences of the available frames.
*/
public class MetaMorphReaderFilter extends AbstractFilter {
/** version number for serialization */
private static final long serialVersionUID = 5382801139981082490L;
/** Sequence name property */
private static final String SEQ_NAME = "SequenceName";
/** zero-length string array for list to array conversion */
private static final String[] STRING_0 = new String[0];
/**
* Constructs a new MetaMorphReaderFilter
.
*/
public MetaMorphReaderFilter() {
super("MetaMorph Series Reader", MetaMorphReaderFilter.class.getName());
this.outputs.add(new FilterOutput(StringPipeItem.class, "The name of the sequence",
"sequence_name"));
this.outputs.add(new FilterOutput(TimeSeriesPipeItem.class, "A series of time stamps",
"time_series"));
this.outputs.add(new FilterOutput(ImageArrayPipeItem.class,
"The raw images from Metamorph", "raw_images"));
makeRenderer();
ImageIO.scanForPlugins();
}
/**
* Duplicates the filter including all of its settings, but returns an independent object.
*
* @return the duplicated object
*/
@Override public AbstractFilter duplicate() {
return new MetaMorphReaderFilter();
}
/**
* Performs the filter operation.
*
* @param executor the FilterTreeExecutor
that is executing the filter
* @param pipe a pipe containing the input data items
* @throws FilterException if the filter cannot complete
*/
@Override public void filter(final FilterTreeExecutor executor, final Pipe pipe)
throws FilterException {
String[] sequences;
String name;
StringPipeItem string;
TimeSeriesPipeItem series;
ImageArrayPipeItem images;
BufferedImage[][] raw;
validateInputs(pipe);
executor.indicateProgress(1);
// Scan for available sequences
sequences = getSequenceNames(pipe.getDir());
if (sequences.length == 0) {
throw new FilterException("No metaMorph sequences to read.");
}
executor.indicateProgress(2);
// Figure out which sequence we're analyzing
name = identifySequence(sequences);
if (name == null) {
throw new FilterException("No MetaMorph sequence to read.");
}
executor.indicateProgress(3);
string = new StringPipeItem(this.outputs.get(0).getKey(), "Sequence name", pipe);
string.setData(name);
pipe.add(string);
executor.indicateProgress(4);
series = new TimeSeriesPipeItem(this.outputs.get(1).getKey(), "Time series", pipe);
pipe.add(series);
executor.indicateProgress(5);
raw = runFilter(executor, pipe.getDir(), name, series);
if (raw == null) {
throw new FilterException("Unable to load raw images.");
}
images = new ImageArrayPipeItem(this.outputs.get(2).getKey(), "Raw images (TIF format)",
pipe, "t", "z", raw.length, raw[0].length, "tif");
pipe.add(images);
executor.indicateProgress(79);
for (int x = 0; x < raw.length; x++) {
for (int y = 0; y < raw[x].length; y++) {
images.setImage(x, y, raw[x][y]);
}
}
executor.indicateProgress(80);
if (!executor.isCancelled()) {
pipe.save(executor);
}
executor.indicateProgress(100);
}
/**
* Gets the names of all video sequences in the directory.
*
* @param sourceDir the directory in which to scan
* @return the names of the sequences
*/
private String[] getSequenceNames(final File sourceDir) {
List names;
File[] files;
String name;
String lower;
int index;
files = sourceDir.listFiles();
names = new ArrayList(files.length);
for (File file : files) {
name = file.getName();
lower = name.toLowerCase(Locale.US);
index = lower.indexOf(".nd");
if (index != -1) {
name = name.substring(0, index);
if (!names.contains(name)) {
names.add(name);
}
}
}
return names.toArray(STRING_0);
}
/**
* Given a nonempty list of available sequence names, determine which we want to process.
*
* @param sequences the list of available sequence names
* @return the selected sequence name
* @throws FilterException if the user canceled during sequence selection
*/
private String identifySequence(final String[] sequences) throws FilterException {
String old;
String name;
old = getProperty(SEQ_NAME);
if (sequences.length > 1) {
if (old == null) {
name = promptForSeqname(sequences);
if (name == null) {
throw new FilterException("No metaMorph sequence to read.");
}
setProperty(SEQ_NAME, name);
} else {
boolean hit = false;
name = old;
for (String test : sequences) {
if (name.equals(test)) {
hit = true;
break;
}
}
if (!hit) {
// The name in the attributes is not valid
name = promptForSeqname(sequences);
if (name == null) {
throw new FilterException("No metaMorph sequence to read.");
}
setProperty(SEQ_NAME, name);
}
}
} else {
name = sequences[0];
if ((old == null) || (!old.equals(name))) {
setProperty(SEQ_NAME, name);
}
}
return name;
}
/**
* Prompts the user to choose from a list of available sequences.
*
* @param sequences the list of names of the available sequences
* @return the selected sequence name
*/
private String promptForSeqname(final String[] sequences) {
int index;
String name;
index = JOptionPane.showOptionDialog(null, "Which sequence would you like to analyze?",
"Load MetaMorph series", JOptionPane.OK_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE, null, sequences, null);
if (index == JOptionPane.CLOSED_OPTION) {
name = null;
} else {
name = sequences[index];
}
return name;
}
/**
* Runs the filter, reading the source Metamorph TIF files and extracting an array of images,
* the first dimension of which is time, and the second dimension of which is z plane.
*
* @param executor the FilterTreeExecutor
that is executing the filter
* @param sourceDir the directory from which to load Metamorph files
* @param seqName the sequence name to read
* @param series the time series to build as data values are read
* @return the array of extracted images
* @throws FilterException if the filter cannot complete
*/
private BufferedImage[][] runFilter(final FilterTreeExecutor executor, final File sourceDir,
final String seqName, final TimeSeriesPipeItem series) throws FilterException {
List files;
List actual;
String test;
String lower;
boolean found;
BufferedImage[][] images;
// Get the list of files we should scan
files = listFiles(sourceDir, seqName);
executor.indicateProgress(6);
if (files.isEmpty()) {
throw new FilterException("No metaMorph data files found.");
}
// Get an ordered list of the files representing time points
actual = new ArrayList(files.size());
for (int t = 1; t <= files.size(); t++) {
if (executor.isCancelled()) {
break;
}
test = "_t" + t + ".tif";
found = false;
for (File file : files) {
lower = file.getName().toLowerCase(Locale.getDefault());
if (lower.endsWith(test)) {
actual.add(file);
found = true;
break;
}
}
if (!found) {
break;
}
}
executor.indicateProgress(7);
if (actual.isEmpty()) {
throw new FilterException("No metaMorph data files found.");
}
images = new BufferedImage[actual.size()][];
// Load the time points
if (!executor.isCancelled()) {
for (int t = 1; t <= actual.size(); t++) {
images[t - 1] = loadTimePoint(actual.get(t - 1), t, series);
executor.indicateProgress(8 + (t * 70 / actual.size()));
}
}
// Set up time point subsequences
return images;
}
/**
* Loads a TIF file which may contain more than one image plane.
*
* @param file the file to read
* @param timeIndex the time index
* @param series the time series to build as data values are read
* @return the set of loaded Z planes for the specified time point
* @throws FilterException if the filter cannot complete
*/
private BufferedImage[] loadTimePoint(final File file, final int timeIndex,
final TimeSeriesPipeItem series) throws FilterException {
ClassList list;
ImageReader reader;
int numPlanes;
int width;
int height;
int bpp;
int channels;
int bytesPer;
byte[] data;
int[] combined;
int type;
BufferedImage[] planes;
int index;
Map meta;
Object obj;
LocalTime time;
list = new ClassList(IFormatReader.class);
list.addClass(MetamorphTiffReader.class);
reader = new ImageReader(list);
try {
reader.setId(file.getAbsolutePath());
width = reader.getSizeX();
height = reader.getSizeY();
bpp = reader.getBitsPerPixel();
// Extract the time of the exposure
meta = reader.getGlobalMetadata();
if (meta == null) {
time = new LocalTime();
time.setMillis(timeIndex);
} else {
obj = meta.get("acquisition-time-local");
if (obj == null) {
obj = meta.get("DateTime");
if (obj == null) {
obj = meta.get("modification-time-local");
}
}
if (obj == null) {
time = new LocalTime();
time.setMillis(timeIndex);
} else {
time = extractTime(obj.toString());
}
}
series.addTimePoint(time);
bytesPer = (bpp + 7) / 8;
channels = reader.getRGBChannelCount();
numPlanes = reader.getImageCount();
planes = new BufferedImage[numPlanes];
for (int z = 0; z < numPlanes; z++) {
data = reader.openBytes(z);
combined = new int[data.length / bytesPer]; // NOPMD SRB
index = 0;
for (int i = 0; i < combined.length; i++) {
combined[i] = data[index] & 0x00FF;
for (int j = 1; j < bytesPer; j++) {
combined[i] += (data[index + j] & 0x00FF) << (8 * j);
}
index += bytesPer;
}
if (channels == 1) {
switch (bytesPer) {
case 1:
type = BufferedImage.TYPE_BYTE_GRAY;
break;
case 2:
type = BufferedImage.TYPE_USHORT_GRAY;
break;
default:
continue;
}
} else if (channels == 3) {
switch (bytesPer) {
case 3:
type = BufferedImage.TYPE_INT_RGB;
break;
case 4:
type = BufferedImage.TYPE_INT_ARGB;
break;
default:
continue;
}
} else {
continue;
}
planes[z] = new BufferedImage(width, height, type); // NOPMD SRB
index = 0;
for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
planes[z].getRaster().setSample(x, y, 0, combined[index]);
index++;
}
}
}
} catch (Exception e) {
LOG.log(Level.WARNING, "Exception reading file", e);
throw new FilterException("Exception reading file", e);
}
return planes;
}
/**
* Retrieves a list of the source files in the target directory for the target sequence.
*
* @param dir the directory in which to search
* @param name the sequence name
* @return the list of files
*/
private List listFiles(final File dir, final String name) {
List list;
File[] files;
String lower;
// list the files and cull those that are not relevant
files = dir.listFiles();
list = new ArrayList(files.length);
for (int i = 0; i < files.length; i++) {
lower = files[i].getName().toLowerCase(Locale.getDefault());
if (files[i].getName().startsWith(name) && (!files[i].isDirectory())
&& (!lower.endsWith(".nd")) && (!lower.contains("_thumb_"))) {
list.add(files[i]);
}
}
return list;
}
/**
* Converts a timestamp string into a LocalTime
. The input data will be in the
* format: '20100902 08:09:13.787'.
*
* @param time the time string to parses
* @return the fixed time
* @throws FilterException if the timestamp cannot be parsed
*/
private LocalTime extractTime(final String time) throws FilterException {
LocalTime fixed;
fixed = new LocalTime();
if ((time.length() >= 19) && (time.charAt(8) == ' ') && (time.charAt(11) == ':')
&& (time.charAt(14) == ':') && (time.charAt(17) == '.')) {
try {
fixed.setYear(Integer.parseInt(time.substring(0, 4)));
fixed.setMonth(Integer.parseInt(time.substring(4, 6)));
fixed.setDay(Integer.parseInt(time.substring(6, 8)));
fixed.setHour(Integer.parseInt(time.substring(9, 11)));
fixed.setMinute(Integer.parseInt(time.substring(12, 14)));
fixed.setSecond(Integer.parseInt(time.substring(15, 17)));
fixed.setMillis(Integer.parseInt(time.substring(18)));
} catch (NumberFormatException e) {
throw new FilterException("Invalid timestamp: '" + time + "'", e);
}
} else {
throw new FilterException("Invalid timestamp: '" + time + "'");
}
return fixed;
}
/**
* Generates the string representation of the filter.
*
* @return the string representation
*/
@Override public String toString() {
return "MetaMorphReaderFilter";
}
}