package gdsc.smlm.ij;
import java.awt.Rectangle;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.concurrent.ArrayBlockingQueue;
import com.thoughtworks.xstream.annotations.XStreamOmitField;
import gdsc.core.ij.Utils;
import gdsc.smlm.ij.utils.ImageConverter;
import gdsc.smlm.ij.utils.SeriesOpener;
import gdsc.smlm.results.ImageSource;
import ij.IJ;
import ij.ImagePlus;
import ij.io.Opener;
/*-----------------------------------------------------------------------------
* GDSC SMLM Software
*
* Copyright (C) 2013 Alex Herbert
* Genome Damage and Stability Centre
* University of Sussex, UK
*
* 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 3 of the License, or
* (at your option) any later version.
*---------------------------------------------------------------------------*/
/**
* Represent a series of image files as a results source. Supports all greyscale images. Only processes channel 0 of
* 32-bit colour images.
* <p>
* Assumes that the width,height,depth dimensions of each file are the same. The depth for the last image can be less
* (i.e. the last of the series) but the {@link #getFrames()} method will return an incorrect value.
* <p>
* Support for the {@link #get(int)} and {@link #get(int, Rectangle)} methods is only provided for TIFF images if the
* images are stacks.
*/
public class SeriesImageSource extends ImageSource
{
private class NextSource
{
final InputStream is;
final int image;
public NextSource(InputStream is, int image)
{
this.is = is;
this.image = image;
}
public NextSource()
{
this(null, -1);
}
}
private class NextImage
{
final Object[] imageArray;
final int imageSize;
final int image;
public NextImage(Object[] imageArray, int imageSize, int image)
{
this.imageArray = imageArray;
this.imageSize = imageSize;
this.image = image;
}
public NextImage()
{
this(null, 0, -1);
}
}
/**
* Add source images to the queue to be read.
* <p>
* If the series is all TIFF images then we can open the file and read it into memory.
*/
private class SourceWorker implements Runnable
{
volatile boolean run = true;
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run()
{
try
{
for (int currentImage = 0; run && currentImage < images.size(); currentImage++)
{
InputStream is = null;
// For a TIFF series ImageJ can open as input stream. We support this by using this
// thread to read the file from disk sequentially and cache into memory. The images
// can then be opened by multiple threads without IO contention.
if (isTiffSeries)
{
final String path = images.get(currentImage);
try
{
//System.out.println("Reading " + images.get(currentImage));
FileInputStream fis = new FileInputStream(path);
byte[] buf = new byte[fis.available()];
int read = fis.read(buf);
fis.close();
is = new ByteArrayInputStream(buf, 0, read);
}
catch (Exception e)
{
// TODO - handle appropriately
// At the moment if we ignore this then the ImageWorker will open the file
// rather than process from the memory stream.
System.out.println(e.toString());
}
}
sourceQueue.put(new NextSource(is, currentImage));
}
}
catch (InterruptedException e)
{
// This is from the queue put method, possibly an interrupt on the queue or thread?
System.out.println(e.toString());
}
// Add jobs to shutdown all the workers
try
{
for (int i = 0; run && i < workers.size(); i++)
sourceQueue.put(new NextSource());
}
catch (InterruptedException e)
{
// This is from the queue put method, possibly an interrupt on the queue or thread?
// TODO - How should this be handled?
System.out.println(e.toString());
}
run = false;
}
}
private class ImageWorker implements Runnable
{
final int id;
volatile boolean run = true;
public ImageWorker(int id)
{
this.id = id;
}
/*
* (non-Javadoc)
*
* @see java.lang.Runnable#run()
*/
public void run()
{
try
{
while (run)
{
NextSource nextSource = sourceQueue.take();
if (nextSource == null || !run)
break;
final int currentImage = nextSource.image;
if (currentImage == -1)
break;
// Open the image
Opener opener = new Opener();
opener.setSilentMode(true);
Utils.setShowProgress(false);
if (logProgress)
{
long time = System.currentTimeMillis();
if (time - lastTime > 500)
{
lastTime = time;
IJ.log("Opening " + images.get(currentImage));
}
}
ImagePlus imp;
if (nextSource.is != null)
{
//System.out.println(id + ": Processing " + images.get(currentImage));
imp = opener.openTiff(nextSource.is, images.get(currentImage));
}
else
{
//System.out.println(id + ": Opening " + images.get(currentImage));
imp = opener.openImage(images.get(currentImage));
}
Utils.setShowProgress(true);
//System.out.println(id + ": Opened " + images.get(currentImage));
Object[] imageArray = null;
int currentImageSize = 0;
if (imp != null)
{
imageArray = imp.getImageStack().getImageArray();
currentImageSize = imp.getStackSize();
// Initialise dimensions on the first valid image
if (width == 0)
setDimensions(imp.getWidth(), imp.getHeight(), currentImageSize);
}
imageQueue.put(new NextImage(imageArray, currentImageSize, currentImage));
}
}
catch (Exception e)
{
// TODO - handle appropriately
System.out.println(id + ": " + e.toString());
}
finally
{
run = false;
// Signal that no more images are available
imageQueue.offer(new NextImage());
}
}
}
private ArrayList<String> images;
public final boolean isTiffSeries;
@XStreamOmitField
private int maxz;
// Used for sequential read
@XStreamOmitField
private Object[] imageArray = null;
@XStreamOmitField
private int nextImage;
@XStreamOmitField
private NextImage[] nextImages;
@XStreamOmitField
private int currentSlice;
@XStreamOmitField
private int currentImageSize;
// Used for frame-based read
@XStreamOmitField
private Object[] lastImageArray = null;
@XStreamOmitField
private int lastImage;
@XStreamOmitField
private int lastImageSize;
@XStreamOmitField
private boolean logProgress = false;
@XStreamOmitField
private long lastTime = 0;
private int numberOfThreads = 1;
@XStreamOmitField
private ArrayBlockingQueue<NextSource> sourceQueue;
@XStreamOmitField
private ArrayBlockingQueue<NextImage> imageQueue;
// Used to queue the files from disk. This is sequential so only one thread is required.
@XStreamOmitField
private SourceWorker sourceWorker = null;
@XStreamOmitField
private Thread sourceThread = null;
// Used to process the files into images
@XStreamOmitField
private ArrayList<ImageWorker> workers = null;
@XStreamOmitField
private ArrayList<Thread> threads = null;
/**
* Create a new image source using the given image series
*
* @param name
* @param path
*/
public SeriesImageSource(String name, SeriesOpener series)
{
super(name);
images = new ArrayList<String>();
if (series != null)
{
for (String imageName : series.getImageList())
{
images.add(new File(series.getPath(), imageName).getPath());
}
}
isTiffSeries = isTiffSeries();
}
/**
* Create a new image source using the directory containing the images.
* <p>
* The directory is opened using {@link gdsc.smlm.ij.utils.SeriesOpener }
*
* @param name
* @param path
*/
public SeriesImageSource(String name, String directory)
{
this(name, new SeriesOpener(directory, false, 1));
}
/**
* Create a new image source using the paths to the images
*
* @param name
* @param filenames
* the full path names of the image files
*/
public SeriesImageSource(String name, String[] filenames)
{
super(name);
images = new ArrayList<String>();
images.addAll(Arrays.asList(filenames));
isTiffSeries = isTiffSeries();
}
private boolean isTiffSeries()
{
Opener opener = new Opener();
for (String path : images)
if (opener.getFileType(path) != Opener.TIFF)
return false;
return true;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.results.ImageSource#openSource()
*/
@Override
public boolean openSource()
{
// reset
close();
if (images.isEmpty())
return false;
// Create the queue for loading the images
createQueue();
return getNextImage() != null;
}
/**
* Creates a background thread to open the images sequentially
*/
private void createQueue()
{
nextImages = new NextImage[images.size()];
final int nThreads = numberOfThreads;
// A blocking queue is used so that the threads do not read too many images in advance
sourceQueue = new ArrayBlockingQueue<NextSource>(nThreads);
// Start a thread to queue up the images
sourceWorker = new SourceWorker();
sourceThread = new Thread(sourceWorker);
sourceThread.start();
// A blocking queue is used so that the threads do not read too many images in advance
imageQueue = new ArrayBlockingQueue<NextImage>(nThreads + 2);
workers = new ArrayList<ImageWorker>(nThreads);
threads = new ArrayList<Thread>(nThreads);
for (int i = 0; i < nThreads; i++)
{
ImageWorker worker = new ImageWorker(i + 1);
workers.add(worker);
Thread thread = new Thread(worker);
threads.add(thread);
thread.start();
}
}
/**
* Close the background thread
*/
private void closeQueue()
{
if (threads != null)
{
// Signal the workers to stop
sourceWorker.run = false;
for (ImageWorker worker : workers)
worker.run = false;
// Prevent processing more source images
sourceQueue.clear();
// Ensure any images already waiting on a blocked queue can be added
imageQueue.clear();
// Send shutdown signals to anything for another source to process
for (int i = 0; i < workers.size(); i++)
sourceQueue.offer(new NextSource());
// Join the threads and then set all to null
try
{
// This thread will be waiting on sourceQueue.put() so it should be finished by now
sourceThread.join();
}
catch (InterruptedException e)
{
// Ignore thread errors
//e.printStackTrace();
}
for (Thread thread : threads)
{
try
{
// This thread will be waiting on imageQueue.put() (which has been cleared)
// or sourceQueue.take() which contains shutdown signals, it should be finished by now
thread.join();
}
catch (InterruptedException e)
{
// Ignore thread errors
//e.printStackTrace();
}
}
sourceThread = null;
threads.clear();
threads = null;
workers.clear();
workers = null;
imageQueue = null;
sourceQueue = null;
}
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.results.ImageSource#close()
*/
public void close()
{
setDimensions(0, 0, 0);
imageArray = lastImageArray = null;
nextImage = currentSlice = currentImageSize = lastImage = lastImageSize = 0;
closeQueue();
}
private Object[] getNextImage()
{
imageArray = null;
currentSlice = currentImageSize = 0;
if (nextImage < nextImages.length)
{
try
{
for (;;)
{
// Images may be out of order due to multiple thread processing.
// Check if we have processed the next image we need.
NextImage next = nextImages[nextImage];
if (next != null)
{
// Clear memory
nextImages[nextImage] = null;
nextImage++;
// Check if there is image data. It may be null if the image was invalid
if (next.imageArray != null)
{
imageArray = next.imageArray;
currentImageSize = next.imageSize;
//System.out.println("Found image: " + images.get(next.image));
// Fill cache
lastImageArray = imageArray;
lastImageSize = currentImageSize;
lastImage = next.image;
return imageArray;
}
else
{
// Process the next stored image
continue;
}
}
// We are still awaiting the next image.
// Get the images processed by the worker threads.
next = imageQueue.poll();
// If there is nothing then check if any workers are alive
if (next == null && workersRunning())
{
// This will block until something produces an image
next = imageQueue.take();
}
if (next != null)
{
// -1 is used when the worker has finished
if (next.image != -1)
{
// Valid image so store it
nextImages[next.image] = next;
}
continue;
}
// Nothing is alive producing images so break
break;
}
}
catch (InterruptedException e)
{
}
}
return imageArray;
}
private boolean workersRunning()
{
for (ImageWorker worker : workers)
if (worker.run)
return true;
return false;
}
private void setDimensions(int maxx, int maxy, int maxz)
{
width = maxx;
height = maxy;
this.maxz = maxz;
// This will be wrong if the stacks are different sizes or images are missing
frames = maxz * images.size();
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.results.ImageSource#nextFrame(java.awt.Rectangle)
*/
@Override
protected float[] nextFrame(Rectangle bounds)
{
// Rolling access
if (imageArray != null)
{
// Check if all frames have been accessed in the current image
if (currentSlice >= currentImageSize)
{
// If no more images then return null
if (getNextImage() == null)
return null;
}
return ImageConverter.getData(imageArray[currentSlice++], width, height, bounds, null);
}
return null;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.results.ImageSource#getFrame(int, java.awt.Rectangle)
*/
@Override
protected float[] getFrame(int frame, Rectangle bounds)
{
if (maxz == 0 || frame < 1)
return null;
// Calculate the required image and slice
int image = (frame - 1) / maxz;
int slice = (frame - 1) % maxz;
// Return from the cache if it exists
if (image != lastImage || lastImageArray == null)
{
lastImageArray = null;
lastImageSize = 0;
if (image < images.size())
{
ImagePlus imp = IJ.openImage(images.get(image++));
if (imp != null)
{
lastImageArray = imp.getImageStack().getImageArray();
lastImageSize = imp.getStackSize();
}
}
}
lastImage = image;
if (lastImageArray != null)
{
if (slice < lastImageSize)
{
return ImageConverter.getData(lastImageArray[slice], width, height, bounds, null);
}
}
return null;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.results.ImageSource#isValid(int)
*/
@Override
public boolean isValid(int frame)
{
return frame > 0 && frame <= frames;
}
/*
* (non-Javadoc)
*
* @see gdsc.smlm.results.ImageSource#toString()
*/
@Override
public String toString()
{
String s = super.toString();
if (!images.isEmpty())
s += String.format(" (%d images: %s ...)", images.size(), images.get(0));
return s;
}
/**
* @return If true send progress to the ImageJ log
*/
public boolean isLogProgress()
{
return logProgress;
}
/**
* @param logProgress
* Set to true to send progress to the ImageJ log
*/
public void setLogProgress(boolean logProgress)
{
this.logProgress = logProgress;
}
/**
* @return The number of background threads to use for opening images
*/
public int getNumberOfThreads()
{
return numberOfThreads;
}
/**
* Set the number of background threads to use for opening images
*
* @param numberOfThreads
* The number of background threads to use for opening images
*/
public void setNumberOfThreads(int numberOfThreads)
{
this.numberOfThreads = Math.max(1, numberOfThreads);
}
}