/*
* #%L
* Bio-Formats Plugins for ImageJ: a collection of ImageJ plugins including the
* Bio-Formats Importer, Bio-Formats Exporter, Bio-Formats Macro Extensions,
* Data Browser and Stack Slicer.
* %%
* Copyright (C) 2006 - 2015 Open Microscopy Environment:
* - Board of Regents of the University of Wisconsin-Madison
* - Glencoe Software, Inc.
* - University of Dundee
* %%
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as
* published by the Free Software Foundation, either version 2 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public
* License along with this program. If not, see
* <http://www.gnu.org/licenses/gpl-2.0.html>.
* #L%
*/
package loci.plugins.in;
import ij.ImagePlus;
import ij.ImageStack;
import ij.io.FileInfo;
import ij.process.ImageProcessor;
import ij.process.LUT;
import java.awt.image.ColorModel;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Vector;
import loci.common.Location;
import loci.common.Region;
import loci.common.StatusEvent;
import loci.common.StatusListener;
import loci.common.StatusReporter;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.formats.FilePattern;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.IFormatReader;
import loci.formats.Modulo;
import loci.formats.meta.IMetadata;
import loci.formats.services.OMEXMLService;
import loci.plugins.Slicer;
import loci.plugins.util.BFVirtualStack;
import loci.plugins.util.ImageProcessorReader;
import loci.plugins.util.LuraWave;
import loci.plugins.util.VirtualImagePlus;
/**
* A high-level reader for {@link ij.ImagePlus} objects.
*/
public class ImagePlusReader implements StatusReporter {
// -- Constants --
/** Special property for storing series number associated with the image. */
public static final String PROP_SERIES = "Series";
/** Special property prefix for storing planar LUTs. */
public static final String PROP_LUT = "LUT-";
// -- Fields --
/**
* Import preparation process managing Bio-Formats readers and other state.
*/
protected ImportProcess process;
protected List<StatusListener> listeners = new Vector<StatusListener>();
// -- Constructors --
/**
* Constructs an ImagePlusReader with the default options.
* @throws IOException if the default options cannot be determined.
*/
public ImagePlusReader() throws IOException {
this(new ImportProcess());
}
/**
* Constructs an ImagePlusReader with the
* given complete import preparation process.
*/
public ImagePlusReader(ImportProcess process) {
this.process = process;
}
// -- ImagePlusReader methods --
/**
* Opens one or more {@link ImagePlus} objects
* corresponding to the reader's associated options.
*/
public ImagePlus[] openImagePlus() throws FormatException, IOException {
List<ImagePlus> imps = readImages();
return imps.toArray(new ImagePlus[0]);
}
public ImagePlus[] openThumbImagePlus() throws FormatException, IOException {
List<ImagePlus> imps = readThumbImages();
return imps.toArray(new ImagePlus[imps.size()]);
}
// -- StatusReporter methods --
@Override
public void addStatusListener(StatusListener l) {
listeners.add(l);
}
@Override
public void removeStatusListener(StatusListener l) {
listeners.remove(l);
}
@Override
public void notifyListeners(StatusEvent e) {
for (StatusListener l : listeners) l.statusUpdated(e);
}
// -- Utility methods --
/**
* Creates an {@link ImagePlus} from the given image processors.
*
* @param title The title for the image.
* @param procs List of image processors to compile into an image.
*/
public static ImagePlus createImage(String title, List<ImageProcessor> procs) {
final List<LUT> luts = new ArrayList<LUT>();
final ImageStack stack = createStack(procs, null, luts);
return createImage(title, stack, luts);
}
/**
* Creates an {@link ImagePlus} from the given image stack.
*
* @param title The title for the image.
* @param stack The image stack containing the image planes.
* @param luts Optional list of plane-specific LUTs
* to store as image properties, for later use.
*/
public static ImagePlus createImage(String title,
ImageStack stack, List<LUT> luts)
{
final ImagePlus imp = new ImagePlus(title, stack);
saveLUTs(imp, luts);
return imp;
}
/**
* Creates an image stack from the given image processors.
*
* @param procs List of image processors to compile into a stack.
* @param labels Optional list of labels, one per plane.
* @param luts Optional list for storing plane-specific LUTs, for later use.
*/
public static ImageStack createStack(List<ImageProcessor> procs,
List<String> labels, List<LUT> luts)
{
if (procs == null || procs.size() == 0) return null;
final ImageProcessor ip0 = procs.get(0);
final ImageStack stack = new ImageStack(ip0.getWidth(), ip0.getHeight());
// construct image stack from list of image processors
for (int i=0; i<procs.size(); i++) {
final ImageProcessor ip = procs.get(i);
final String label = labels == null ? null : labels.get(i);
// HACK: ImageProcessorReader always assigns an ij.process.LUT object
// as the color model. If we don't get one, we know ImageJ created a
// default color model instead, which we can discard.
if (luts != null) {
final ColorModel cm = ip.getColorModel();
if (cm instanceof LUT) {
// plane has custom LUT attached; save it to the list
final LUT lut = (LUT) cm;
luts.add(lut);
// discard custom LUT from ImageProcessor
ip.setColorModel(ip.getDefaultColorModel());
}
else {
// no LUT attached; save a placeholder
luts.add(null);
}
}
// add plane to image stack
stack.addSlice(label, ip);
}
return stack;
}
// -- Helper methods - image reading --
private List<ImagePlus> readImages() throws FormatException, IOException {
return readImages(false);
}
private List<ImagePlus> readThumbImages() throws FormatException, IOException
{
return readImages(true);
}
private List<ImagePlus> readImages(boolean thumbnail)
throws FormatException, IOException
{
final ImporterOptions options = process.getOptions();
final ImageProcessorReader reader = process.getReader();
List<ImagePlus> imps = new ArrayList<ImagePlus>();
// beginning timing
startTiming();
// read in each image series
for (int s=0; s<reader.getSeriesCount(); s++) {
if (!options.isSeriesOn(s)) continue;
final ImagePlus imp = readImage(s, thumbnail);
imps.add(imp);
}
// concatenate compatible images
imps = concatenate(imps);
// colorize images, as appropriate
imps = applyColors(imps);
// split dimensions, as appropriate
imps = splitDims(imps);
// set virtual stack's reference count to match # of image windows
// in this case, these is one window per enabled image series
// when all image windows are closed, the Bio-Formats reader is closed
if (options.isVirtual()) {
process.getVirtualReader().setRefCount(imps.size());
}
// end timing
finishTiming();
return imps;
}
private ImagePlus readImage(int s, boolean thumbnail)
throws FormatException, IOException
{
final ImporterOptions options = process.getOptions();
final int zCount = process.getZCount(s);
final int cCount = process.getCCount(s);
final int tCount = process.getTCount(s);
final List<LUT> luts = new ArrayList<LUT>();
// create image stack
final ImageStack stack;
if (options.isVirtual()) stack = createVirtualStack(process, s, luts);
else stack = readPlanes(process, s, luts, thumbnail);
notifyListeners(new StatusEvent(1, 1, "Creating image"));
// create title
final String seriesName = process.getOMEMetadata().getImageName(s);
final String file = process.getCurrentFile();
final IFormatReader reader = process.getReader();
final String title = constructImageTitle(reader,
file, seriesName, options.isGroupFiles());
// create image
final ImagePlus imp;
if (stack.isVirtual()) {
VirtualImagePlus vip = new VirtualImagePlus(title, stack);
vip.setReader(reader);
imp = vip;
saveLUTs(imp, luts);
}
else imp = createImage(title, stack, luts);
// configure image
// place metadata key/value pairs in ImageJ's info field
final String metadata = process.getOriginalMetadata().toString();
imp.setProperty("Info", metadata);
imp.setProperty(PROP_SERIES, s);
// retrieve the spatial calibration information, if available
final FileInfo fi = createFileInfo();
new Calibrator(process).applyCalibration(imp);
imp.setFileInfo(fi);
imp.setDimensions(cCount, zCount, tCount);
// open as a hyperstack, as appropriate
final boolean hyper = !options.isViewStandard();
imp.setOpenAsHyperStack(hyper);
return imp;
}
private ImageStack createVirtualStack(ImportProcess process, int s,
List<LUT> luts) throws FormatException, IOException
{
final ImporterOptions options = process.getOptions();
final ImageProcessorReader reader = process.getReader();
reader.setSeries(s);
final int zCount = process.getZCount(s);
final int cCount = process.getCCount(s);
final int tCount = process.getTCount(s);
final IMetadata meta = process.getOMEMetadata();
final int imageCount = reader.getImageCount();
// CTR FIXME: Make virtual stack work with different color modes?
final BFVirtualStack virtualStack = new BFVirtualStack(options.getId(),
reader, false, false, false);
for (int i=0; i<imageCount; i++) {
final String label = constructSliceLabel(i,
reader, meta, s, zCount, cCount, tCount);
virtualStack.addSlice(label);
}
if (luts != null) {
for (int c=0; c<cCount; c++) {
int index = reader.getIndex(0, c, 0);
ImageProcessor ip = reader.openProcessors(index)[0];
final ColorModel cm = ip.getColorModel();
final LUT lut = cm instanceof LUT ? (LUT) cm : null;
luts.add(lut);
}
}
return virtualStack;
}
private ImageStack readPlanes(ImportProcess process, int s, List<LUT> luts,
boolean thumbnail)
throws FormatException, IOException
{
final ImageProcessorReader reader = process.getReader();
reader.setSeries(s);
final int zCount = process.getZCount(s);
final int cCount = process.getCCount(s);
final int tCount = process.getTCount(s);
final IMetadata meta = process.getOMEMetadata();
// get list of planes to load
final boolean[] load = getPlanesToLoad(s);
int current = 0, total = 0;
for (int j=0; j<load.length; j++) if (load[j]) total++;
final List<ImageProcessor> procs = new ArrayList<ImageProcessor>();
final List<String> labels = new ArrayList<String>();
// read applicable image planes
final Region region = process.getCropRegion(s);
for (int i=0; i<load.length; i++) {
if (!load[i]) continue;
// limit message update rate
updateTiming(s, current, current++, total);
// get image processor for ith plane
final ImageProcessor[] p = readProcessors(process, i, region, thumbnail);
if (p == null || p.length == 0) {
throw new FormatException("Cannot read plane #" + i);
}
// generate a label for ith plane
final String label = constructSliceLabel(i,
reader, meta, s, zCount, cCount, tCount);
for (ImageProcessor ip : p) {
procs.add(ip);
labels.add(label);
}
}
return createStack(procs, labels, luts);
}
/**
* HACK: This method mainly exists to prompt the user for a missing
* LuraWave license code, in the case of LWF-compressed Flex.
*
* @see ImportProcess#setId()
*/
private ImageProcessor[] readProcessors(ImportProcess process,
int no, Region r, boolean thumbnail) throws FormatException, IOException
{
final ImageProcessorReader reader = process.getReader();
final ImporterOptions options = process.getOptions();
boolean first = true;
for (int i=0; i<LuraWave.MAX_TRIES; i++) {
String code = LuraWave.initLicenseCode();
try {
if (thumbnail) {
return reader.openThumbProcessors(no);
}
return reader.openProcessors(no, r.x, r.y, r.width, r.height);
}
catch (FormatException exc) {
if (options.isQuiet() || options.isWindowless()) throw exc;
if (!LuraWave.isLicenseCodeException(exc)) throw exc;
// prompt user for LuraWave license code
code = LuraWave.promptLicenseCode(code, first);
if (code == null) throw exc;
if (first) first = false;
}
}
throw new FormatException(LuraWave.TOO_MANY_TRIES);
}
// -- Helper methods - image post processing --
private List<ImagePlus> concatenate(List<ImagePlus> imps) {
final ImporterOptions options = process.getOptions();
if (options.isConcatenate()) imps = new Concatenator().concatenate(imps);
return imps;
}
private List<ImagePlus> applyColors(List<ImagePlus> imps) {
return new Colorizer(process).applyColors(imps);
}
private List<ImagePlus> splitDims(List<ImagePlus> imps) {
final ImporterOptions options = process.getOptions();
final boolean sliceC = options.isSplitChannels();
final boolean sliceZ = options.isSplitFocalPlanes();
final boolean sliceT = options.isSplitTimepoints();
if (sliceC || sliceZ || sliceT) {
final String stackOrder = process.getStackOrder();
final List<ImagePlus> slicedImps = new ArrayList<ImagePlus>();
final Slicer slicer = new Slicer();
for (ImagePlus imp : imps) {
final ImagePlus[] results = slicer.reslice(imp,
sliceC, sliceZ, sliceT, stackOrder);
for (ImagePlus result : results) slicedImps.add(result);
}
imps = slicedImps;
}
return imps;
}
// -- Helper methods - timing --
private long startTime, time;
private void startTiming() {
startTime = time = System.currentTimeMillis();
}
private void updateTiming(int s, int i, int current, int total) {
final ImageProcessorReader reader = process.getReader();
long clock = System.currentTimeMillis();
if (clock - time >= 100) {
String sLabel = reader.getSeriesCount() > 1 ?
("series " + (s + 1) + ", ") : "";
String pLabel = "plane " + (i + 1) + "/" + total;
notifyListeners(new StatusEvent("Reading " + sLabel + pLabel));
time = clock;
}
notifyListeners(new StatusEvent(current, total, null));
}
private void finishTiming() {
final ImageProcessorReader reader = process.getReader();
long endTime = System.currentTimeMillis();
double elapsed = (endTime - startTime) / 1000.0;
if (reader.getImageCount() == 1) {
notifyListeners(new StatusEvent("Bio-Formats: " + elapsed + " seconds"));
}
else {
long average = (endTime - startTime) / reader.getImageCount();
notifyListeners(new StatusEvent("Bio-Formats: " +
elapsed + " seconds (" + average + " ms per plane)"));
}
}
// -- Helper methods -- miscellaneous --
private FileInfo createFileInfo() {
final FileInfo fi = new FileInfo();
// populate common FileInfo fields
String idDir = process.getIdLocation() == null ?
null : process.getIdLocation().getParent();
if (idDir != null && !idDir.endsWith(File.separator)) {
idDir += File.separator;
}
fi.fileName = process.getIdName();
fi.directory = idDir;
// dump OME-XML to ImageJ's description field, if available
fi.description = process.getOMEXML();
return fi;
}
private boolean[] getPlanesToLoad(int s) {
final ImageProcessorReader reader = process.getReader();
final boolean[] load = new boolean[reader.getImageCount()];
final int cBegin = process.getCBegin(s);
final int cEnd = process.getCEnd(s);
final int cStep = process.getCStep(s);
final int zBegin = process.getZBegin(s);
final int zEnd = process.getZEnd(s);
final int zStep = process.getZStep(s);
final int tBegin = process.getTBegin(s);
final int tEnd = process.getTEnd(s);
final int tStep = process.getTStep(s);
for (int c=cBegin; c<=cEnd; c+=cStep) {
for (int z=zBegin; z<=zEnd; z+=zStep) {
for (int t=tBegin; t<=tEnd; t+=tStep) {
int index = reader.getIndex(z, c, t);
load[index] = true;
}
}
}
return load;
}
private String constructImageTitle(IFormatReader r,
String file, String seriesName, boolean groupFiles)
{
String[] used = r.getUsedFiles();
String title = file.substring(file.lastIndexOf(File.separator) + 1);
if (used.length > 1 && groupFiles) {
FilePattern fp = new FilePattern(new Location(file));
title = fp.getPattern();
if (title == null) {
title = file;
if (title.indexOf(".") != -1) {
title = title.substring(0, title.lastIndexOf("."));
}
}
title = title.substring(title.lastIndexOf(File.separator) + 1);
}
if (seriesName != null && !file.endsWith(seriesName) &&
r.getSeriesCount() > 1)
{
title += " - " + seriesName;
}
if (title.length() > 128) {
String a = title.substring(0, 62);
String b = title.substring(title.length() - 62);
title = a + "..." + b;
}
return title;
}
private String constructSliceLabel(int ndx, IFormatReader r,
IMetadata meta, int series, int zCount, int cCount, int tCount)
{
r.setSeries(series);
final int[] zct = r.getZCTCoords(ndx);
final int sizeC = r.getSizeC();
final StringBuffer sb = new StringBuffer();
int[] subC;
String[] subCTypes;
Modulo moduloC = r.getModuloC();
if (moduloC.length() > 1) {
subC = new int[] {r.getSizeC() / moduloC.length(), moduloC.length()};
subCTypes = new String[] {moduloC.parentType, moduloC.type};
} else {
subC = new int[] {r.getSizeC()};
subCTypes = new String[] {FormatTools.CHANNEL};
}
boolean first = true;
if (cCount > 1) {
if (first) first = false;
else sb.append("; ");
int[] subCPos = FormatTools.rasterToPosition(subC, zct[1]);
for (int i=0; i<subC.length; i++) {
boolean ch =
subCTypes[i] == null || FormatTools.CHANNEL.equals(subCTypes[i]);
sb.append(ch ? "c" : subCTypes[i]);
sb.append(":");
sb.append(subCPos[i] + 1);
sb.append("/");
sb.append(subC[i]);
if (i < subC.length - 1) sb.append(", ");
}
}
if (zCount > 1) {
if (first) first = false;
else sb.append("; ");
sb.append("z:");
sb.append(zct[0] + 1);
sb.append("/");
sb.append(r.getSizeZ());
}
if (tCount > 1) {
if (first) first = false;
else sb.append("; ");
sb.append("t:");
sb.append(zct[2] + 1);
sb.append("/");
sb.append(r.getSizeT());
}
// put image name at the end, in case it is long
String imageName = meta.getImageName(series);
if (imageName != null && !imageName.trim().equals("")) {
sb.append(" - ");
sb.append(imageName);
}
return sb.toString();
}
private static void saveLUTs(ImagePlus imp, List<LUT> luts) {
// NB: Save individual planar LUTs as properties, for later access.
// This step is necessary because ImageStack.addSlice only extracts the
// pixels from the ImageProcessor, and does not preserve the ColorModel.
// Later, Colorizer can use the LUTs when wrapping into a CompositeImage.
for (int i=0; i<luts.size(); i++) {
final LUT lut = luts.get(i);
if (lut != null) {
imp.setProperty(PROP_LUT + i, lut);
}
}
}
}