package ij.plugin;
import ij.*;
import ij.process.*;
import ij.gui.*;
import ij.io.*;
import ij.plugin.Animator;
import java.awt.*;
import java.awt.image.*;
import java.io.*;
import java.util.*;
import javax.imageio.ImageIO;
/** <pre>
* ImageJ Plugin for reading an AVI file into an image stack
* (one slice per video frame)
*
* Version 2008-07-03 by Michael Schmid, based on a plugin by
* Daniel Marsh and Wayne Rasband
*
* Restrictions and Notes:
* - Only few formats supported:
* - uncompressed 8 bit with palette (=LUT)
* - uncompressed 8 & 16 bit grayscale
* - uncompressed 24 & 32 bit RGB (alpha channel ignored)
* - uncompressed 32 bit AYUV (alpha channel ignored)
* - various YUV 4:2:2 compressed formats
* - png or jpeg-encoded individual frames.
* Note that most MJPG (motion-JPEG) formats are not read correctly.
* - Does not read avi formats with more than one frame per chunk
* - Palette changes during the video not supported
* - Out-of-sequence frames (sequence given by index) not supported
* - Different frame sizes in one file (rcFrame) not supported
* - Conversion of (A)YUV formats to grayscale is non-standard:
* All 255 levels are kept as in the input (i.e. the full dynamic
* range of data from a frame grabber is preserved).
* For standard behavior, use "Brightness&Contrast", Press "Set",
* enter "Min." 16, "Max." 235, and press "Apply".
* Version History:
* 2008-07-03
* - Support for 16bit AVIs coded by MIL (Matrox Imaging Library)
* 2008-06-08
* - Support for png and jpeg/mjpg encoded files added
* - Retrieves animation speed from image frame rate
* - Exception handling without multiple error messages
* 2008-04-29
* - Support for several other formats added, especially some YUV
* (also named YCbCr) formats
* - Uneven chunk sizes fixed
* - Negative biHeight fixed
* - Audio or second video stream don't cause a problem
* - Can read part of a file (specify start & end frame numbers)
* - Can convert YUV and RGB to grayscale (does not convert 8-bit with palette)
* - Can flip vertically
* - Can create a virtual stack
* - Added slice label: time of the frame in the movie
* - Added a public method 'getStack' that does not create an image window
* - More compact code, especially for reading the header (rewritten)
* - In the code, bitmapinfo items have their canonical names
*
* The AVI format looks like this:
* RIFF RIFF HEADER
* |-AVI AVI CHUNK
* |LIST hdrl MAIN AVI HEADER
* | |-avih AVI HEADER
* | |LIST strl STREAM LIST(s) (One per stream)
* | | |-strh STREAM HEADER (Required after above; fourcc type is 'vids' for video stream)
* | | |-strf STREAM FORMAT (for video: BitMapInfo; may also contain palette)
* | | |-strd OPTIONAL -- STREAM DATA (ignored in this plugin)
* | | |-strn OPTIONAL -- STREAM NAME (ignored in this plugin)
* |LIST movi MOVIE DATA
* | | [rec] RECORD DATA (one record per frame for interleaved video; optional, unsupported in this plugin)
* | | |-dataSubchunks RAW DATA: '??wb' for audio, '??db' and '??dc' for uncompressed and
* | compressed video, respectively. "??" denotes stream number, usually "00" or "01"
* |-idx1 AVI INDEX (required by some programs, ignored in this plugin)
* </pre>
*/
public class AVI_Reader extends VirtualStack implements PlugIn {
//four-character codes for avi chunk types
//NOTE: byte sequence is reversed - ints in Intel (little endian) byte order!
private final static int FOURCC_RIFF = 0x46464952; //'RIFF'
private final static int FOURCC_AVI = 0x20495641; //'AVI '
private final static int FOURCC_LIST = 0x5453494c; //'LIST'
private final static int FOURCC_hdrl = 0x6c726468; //'hdrl'
private final static int FOURCC_avih = 0x68697661; //'avih'
private final static int FOURCC_strl = 0x6c727473; //'strl'
private final static int FOURCC_strh = 0x68727473; //'strh'
private final static int FOURCC_strf = 0x66727473; //'strf'
private final static int FOURCC_movi = 0x69766f6d; //'movi'
private final static int FOURCC_rec = 0x20636572; //'rec '
private final static int FOURCC_JUNK = 0x4b4e554a; //'JUNK'
private final static int FOURCC_vids = 0x73646976; //'vids'
private final static int FOURCC_00db = 0x62643030; //'00db'
private final static int FOURCC_00dc = 0x63643030; //'00dc'
//four-character codes for supported compression formats; see fourcc.org
private final static int NO_COMPRESSION = 0; //uncompressed, also 'RGB ', 'RAW '
private final static int NO_COMPRESSION_RGB= 0x20424752; //'RGB ' -a name for uncompressed
private final static int NO_COMPRESSION_RAW= 0x20574152; //'RAW ' -a name for uncompressed
private final static int NO_COMPRESSION_Y800=0x30303859;//'Y800' -a name for 8-bit grayscale
private final static int NO_COMPRESSION_Y8 = 0x20203859; //'Y8 ' -another name for Y800
private final static int NO_COMPRESSION_GREY=0x59455247;//'GREY' -another name for Y800
private final static int NO_COMPRESSION_Y16= 0x20363159; //'Y16 ' -a name for 16-bit uncompressed grayscale
private final static int NO_COMPRESSION_MIL= 0x204c494d; //'MIL ' - Matrox Imaging Library
private final static int AYUV_COMPRESSION = 0x56555941; //'AYUV' -uncompressed, but alpha, Y, U, V bytes
private final static int UYVY_COMPRESSION = 0x59565955; //'UYVY' - 4:2:2 with byte order u y0 v y1
private final static int Y422_COMPRESSION = 0x564E5955; //'Y422' -another name for of UYVY
private final static int UYNV_COMPRESSION = 0x32323459; //'UYNV' -another name for of UYVY
private final static int CYUV_COMPRESSION = 0x76757963; //'cyuv' -as UYVY but not top-down
private final static int V422_COMPRESSION = 0x32323456; //'V422' -as UYVY but not top-down
private final static int YUY2_COMPRESSION = 0x32595559; //'YUY2' - 4:2:2 with byte order y0 u y1 v
private final static int YUNV_COMPRESSION = 0x564E5559; //'YUNV' -another name for YUY2
private final static int YUYV_COMPRESSION = 0x56595559; //'YUYV' -another name for YUY2
private final static int YVYU_COMPRESSION = 0x55595659; //'YVYU' - 4:2:2 with byte order y0 u y1 v
private final static int JPEG_COMPRESSION = 0x6765706a; //'jpeg' JPEG compression of individual frames
private final static int JPEG_COMPRESSION2 = 0x4745504a; //'JPEG' JPEG compression of individual frames
private final static int JPEG_COMPRESSION3 = 0x04; //BI_JPEG: JPEG compression of individual frames
private final static int MJPG_COMPRESSION = 0x47504a4d; //'MJPG' Motion JPEG, also reads compression of individual frames
private final static int PNG_COMPRESSION = 0x20676e70; //'png ' PNG compression of individual frames
private final static int PNG_COMPRESSION2 = 0x20474e50; //'PNG ' PNG compression of individual frames
private final static int PNG_COMPRESSION3 = 0x05; //BI_PNG PNG compression of individual frames
private final static int BITMASK24 = 0x10000; //for 24-bit (in contrast to 8, 16,... not a bitmask)
private final static long SIZE_MASK = 0xffffffffL; //for conversion of sizes from unsigned int to long
//dialog parameters are static so they will be remembered
private static int firstFrameNumber = 1; //the first frame to read
private static int lastFrameNumber = 0; //the last frame to read; 0 means 'read all'
private static boolean convertToGray; //whether to convert color video to grayscale
private static boolean flipVertical; //whether to flip image vertical
private static boolean isVirtual; //whether to open as virtual stack
//the input file
private RandomAccessFile raFile;
private String raFilePath;
private boolean headerOK = false; //whether header has been read
//more avi file properties etc
private int streamNumber; //number of the (first) video stream
private long fileSize = 0; //file size (for progress bar)
private int paddingGranularity = 2; //tags start at even address
//derived from BitMapInfo
private int dataCompression; //data compression type used
private int scanLineSize;
private boolean dataTopDown; //whether data start at top of image
private ColorModel cm;
private boolean variableLength; //compressed (PNG, JPEG) frames have variable length
//for conversion to ImageJ stack
private Vector frameInfos; //for virtual stack: long[] with frame pos&size in file, time(usec)
private ImageStack stack;
private ImagePlus imp;
//for debug messages
private boolean verbose = IJ.debugMode;
private long startTime;
//From AVI Header Chunk
private int dwMicroSecPerFrame;
private int dwMaxBytesPerSec;
private int dwReserved1;
private int dwFlags;
private int dwTotalFrames;
private int dwInitialFrames;
private int dwStreams;
private int dwSuggestedBufferSize;
private int dwWidth;
private int dwHeight;
//From Stream Header Chunk
private int fccStreamHandler;
private int dwStreamFlags;
private int dwPriorityLanguage; //actually 2 16-bit words: wPriority and wLanguage
private int dwStreamInitialFrames;
private int dwStreamScale;
private int dwStreamRate;
private int dwStreamStart;
private int dwStreamLength;
private int dwStreamSuggestedBufferSize;
private int dwStreamQuality;
private int dwStreamSampleSize;
//From Stream Format Chunk: BITMAPINFO contents (40 bytes)
private int biSize; // size of this header in bytes (40)
private int biWidth;
private int biHeight;
private short biPlanes; // no. of color planes: for the formats decoded; here always 1
private short biBitCount; // Bits per Pixel
private int biCompression;
private int biSizeImage; // size of image in bytes (may be 0: if so, calculate)
private int biXPelsPerMeter; // horizontal resolution, pixels/meter (may be 0)
private int biYPelsPerMeter; // vertical resolution, pixels/meter (may be 0)
private int biClrUsed; // no. of colors in palette (if 0, calculate)
private int biClrImportant; // no. of important colors (appear first in palette) (0 means all are important)
/** The plugin is invoked by ImageJ using this method.
* String 'arg' may be used to select the path.
*/
public void run (String arg) {
OpenDialog od = new OpenDialog("Select AVI File", arg); //file dialog
String fileName = od.getFileName();
if (fileName == null) return;
String fileDir = od.getDirectory();
String path = fileDir + fileName;
try {
openAndReadHeader(path); //open and read header
} catch (Exception e) {
IJ.showMessage("AVI Reader", exceptionMessage(e));
return;
}
if (!showDialog(fileName)) return; //ask for parameters
try {
ImageStack stack = makeStack (path, firstFrameNumber, lastFrameNumber,
isVirtual, convertToGray, flipVertical); //read data
} catch (Exception e) {
IJ.showMessage("AVI Reader", exceptionMessage(e));
return;
}
if (stack == null) return;
if (stack.getSize() == 0) {
String rangeText = "";
if (firstFrameNumber>1 || lastFrameNumber!=0)
rangeText = "\nin Range "+firstFrameNumber+
(lastFrameNumber>0 ? " - "+lastFrameNumber : " - end");
IJ.showMessage("AVI Reader","Error: No Frames Found"+rangeText);
return;
}
imp = new ImagePlus(fileName, stack);
if (imp.getBitDepth()==16)
imp.getProcessor().resetMinAndMax();
setFramesPerSecond(imp);
FileInfo fi = new FileInfo();
fi.fileName = fileName;
fi.directory = fileDir;
imp.setFileInfo(fi);
if (arg.equals(""))
imp.show();
IJ.showTime(imp, startTime, "Read AVI in ", stack.getSize());
}
/** Returns the ImagePlus opened by run(). */
public ImagePlus getImagePlus() {
return imp;
}
/** Create an ImageStack from an avi file with given path.
* @param path Directoy+filename of the avi file
* @param firstFrameNumber Number of first frame to read (first frame of the file is 1)
* @param lastFrameNumber Number of last frame to read or 0 for reading all, -1 for all but last...
* @param isVirtual Whether to return a virtual stack
* @param convertToGray Whether to convert color images to grayscale
* @return Returns the stack; null on failure.
* The stack returned may be non-null, but have a length of zero if no suitable frames were found
*/
public ImageStack makeStack (String path, int firstFrameNumber, int lastFrameNumber,
boolean isVirtual, boolean convertToGray, boolean flipVertical) {
this.firstFrameNumber = firstFrameNumber;
this.lastFrameNumber = lastFrameNumber;
this.isVirtual = isVirtual;
this.convertToGray = convertToGray;
this.flipVertical = flipVertical;
String exceptionMessage = null;
IJ.showProgress(.001);
try {
readAVI(path);
} catch (OutOfMemoryError e) {
stack.trim();
IJ.showMessage("AVI Reader", "Out of memory. " + stack.getSize() + " of " + dwTotalFrames + " frames will be opened.");
} catch (Exception e) {
exceptionMessage = exceptionMessage(e);
} finally {
try {
raFile.close();
if (verbose)
IJ.log("File closed.");
} catch (Exception e) {}
IJ.showProgress(1.0);
}
if (exceptionMessage != null) throw new RuntimeException(exceptionMessage);
if (isVirtual && frameInfos != null)
stack = this;
if (stack!=null && cm!=null)
stack.setColorModel(cm);
return stack;
}
/** Returns an ImageProcessor for the specified frame of this virtual stack (if it is one)
where 1<=n<=nslices. Returns null if no virtual stack or no slices.
*/
public synchronized ImageProcessor getProcessor(int n) {
if (frameInfos==null || frameInfos.size()==0 || raFilePath==null)
return null;
if (n<1 || n>frameInfos.size())
throw new IllegalArgumentException("Argument out of range: "+n);
Object pixels = null;
RandomAccessFile rFile = null;
String exceptionMessage = null;
try {
rFile = new RandomAccessFile(new File(raFilePath), "r");
long[] frameInfo = (long[])(frameInfos.get(n-1));
pixels = readFrame(rFile, frameInfo[0], (int)frameInfo[1]);
} catch (Exception e) {
exceptionMessage = exceptionMessage(e);
} finally {
try {
rFile.close();
} catch (Exception e) {}
}
if (exceptionMessage != null) throw new RuntimeException(exceptionMessage);
if (pixels == null) return null; //failed
if (pixels instanceof byte[])
return new ByteProcessor(dwWidth, biHeight, (byte[])pixels, cm);
else if (pixels instanceof short[])
return new ShortProcessor(dwWidth, biHeight, (short[])pixels, cm);
else
return new ColorProcessor(dwWidth, biHeight, (int[])pixels);
}
/** Returns the image width of the virtual stack */
public int getWidth() {
return dwWidth;
}
/** Returns the image height of the virtual stack */
public int getHeight() {
return biHeight;
}
/** Returns the number of images in this virtual stack (if it is one) */
public int getSize() {
if (frameInfos == null) return 0;
else return frameInfos.size();
}
/** Returns the label of the specified slice in this virtual stack (if it is one). */
public String getSliceLabel(int n) {
if (frameInfos==null || n<1 || n>frameInfos.size())
throw new IllegalArgumentException("No Virtual Stack or argument out of range: "+n);
return frameLabel(((long[])(frameInfos.get(n-1)))[2]);
}
/** Deletes the specified image from this virtual stack (if it is one),
* where 1<=n<=nslices. */
public void deleteSlice(int n) {
if (frameInfos==null || frameInfos.size()==0) return;
if (n<1 || n>frameInfos.size())
throw new IllegalArgumentException("Argument out of range: "+n);
frameInfos.removeElementAt(n-1);
}
/** Parameters dialog, returns false on cancel */
private boolean showDialog (String fileName) {
if (lastFrameNumber!=-1)
lastFrameNumber = dwTotalFrames;
if (IJ.macroRunning()) {
firstFrameNumber = 1;
lastFrameNumber = dwTotalFrames;
}
GenericDialog gd = new GenericDialog("AVI Reader");
gd.addNumericField("First Frame: ", firstFrameNumber, 0);
gd.addNumericField("Last Frame: ", lastFrameNumber, 0, 6, "");
gd.addCheckbox("Use Virtual Stack", isVirtual);
gd.addCheckbox("Convert to Grayscale", convertToGray);
gd.addCheckbox("Flip Vertical", flipVertical);
gd.showDialog();
if (gd.wasCanceled()) return false;
firstFrameNumber = (int)gd.getNextNumber();
lastFrameNumber = (int)gd.getNextNumber();
isVirtual = gd.getNextBoolean();
convertToGray = gd.getNextBoolean();
flipVertical = gd.getNextBoolean();
IJ.register(this.getClass());
return true;
}
private void readAVI(String path) throws Exception, IOException {
if (!headerOK) //we have not read the header yet?
openAndReadHeader(path);
startTime += System.currentTimeMillis();//taking previously elapsed time into account
findFourccAndRead(FOURCC_movi, true, fileSize, true); //read movie data
return;
}
/** Open the file with given path and read its header */
private void openAndReadHeader (String path) throws Exception, IOException {
startTime = System.currentTimeMillis();
if (verbose)
IJ.log("OPEN AND READ AVI FILE HEADER "+timeString());
File file = new File(path); // o p e n
raFile = new RandomAccessFile(file, "r");
raFilePath = path;
fileSize = raFile.length();
int fileType = readInt(); // f i l e h e a d e r
if (verbose)
IJ.log("File header: File type='"+fourccString(fileType)+"' (should be 'RIFF')"+timeString());
if (fileType != FOURCC_RIFF)
throw new Exception("Not an AVI file.");
readInt(); //data size
int riffType = readInt();
if (verbose)
IJ.log("File header: RIFF type='"+fourccString(riffType)+"' (should be 'AVI ')");
if (riffType != FOURCC_AVI)
throw new Exception("Not an AVI file.");
findFourccAndRead(FOURCC_hdrl, true, fileSize, true);
startTime -= System.currentTimeMillis(); //becomes minus elapsed Time
headerOK = true;
}
/** Find the next position of fourcc or LIST fourcc and read contents.
* Returns next position
* If not found, throws exception or returns -1 */
private long findFourccAndRead(int fourcc, boolean isList, long endPosition,
boolean throwNotFoundException) throws Exception, IOException {
long nextPos;
boolean contentOk = false;
do {
int type = readType(endPosition);
if (type == 0) { //reached endPosition without finding
if (throwNotFoundException)
throw new Exception("Required item '"+fourccString(fourcc)+"' not found");
else
return -1;
}
long size = readInt() & SIZE_MASK;
nextPos = raFile.getFilePointer() + size;
boolean foundList = false;
if (isList && type == FOURCC_LIST) {
foundList = true;
type = readInt();
}
if (verbose)
IJ.log("Searching for '"+fourccString(fourcc)+"', found "+(foundList?"LIST '":"'")
+fourccString(type)+"' "+posSizeString(nextPos-size, size));
if (type==fourcc) {
contentOk = readContents(fourcc, nextPos);
} else if (verbose)
IJ.log("Discarded '"+fourccString(type)+"': Contents does not fit");
raFile.seek(nextPos);
if (contentOk)
return nextPos; //found and read, breaks the loop
} while (!contentOk);
return nextPos;
}
/** read contents defined by four-cc code; returns true if contens ok */
private boolean readContents (int fourcc, long endPosition) throws Exception, IOException {
switch (fourcc) {
case FOURCC_hdrl:
findFourccAndRead(FOURCC_avih, false, endPosition, true);
findFourccAndRead(FOURCC_strl, true, endPosition, true);
return true;
case FOURCC_avih:
readAviHeader();
return true;
case FOURCC_strl:
long nextPosition = findFourccAndRead(FOURCC_strh, false, endPosition, false);
if (nextPosition<0) return false;
findFourccAndRead(FOURCC_strf, false, endPosition, true);
return true;
case FOURCC_strh:
int streamType = readInt();
if (streamType != FOURCC_vids) {
if (verbose)
IJ.log("Non-video Stream '"+fourccString(streamType)+" skipped");
streamNumber++;
return false;
}
readStreamHeader();
return true;
case FOURCC_strf:
readBitMapInfo(endPosition);
return true;
case FOURCC_movi:
readMovieData(endPosition);
return true;
}
throw new Exception("Program error, type "+fourccString(fourcc));
}
void readAviHeader() throws Exception, IOException {
dwMicroSecPerFrame = readInt();
dwMaxBytesPerSec = readInt();
dwReserved1 = readInt(); //in newer avi formats, this is dwPaddingGranularity?
dwFlags = readInt();
dwTotalFrames = readInt();
dwInitialFrames = readInt();
dwStreams = readInt();
dwSuggestedBufferSize = readInt();
dwWidth = readInt();
dwHeight = readInt();
// dwReserved[4] follows, ignored
if (verbose) {
IJ.log("AVI HEADER (avih):"+timeString());
IJ.log(" dwMicroSecPerFrame=" + dwMicroSecPerFrame);
IJ.log(" dwMaxBytesPerSec=" + dwMaxBytesPerSec);
IJ.log(" dwReserved1=" + dwReserved1);
IJ.log(" dwFlags=" + dwFlags);
IJ.log(" dwTotalFrames=" + dwTotalFrames);
IJ.log(" dwInitialFrames=" + dwInitialFrames);
IJ.log(" dwStreams=" + dwStreams);
IJ.log(" dwSuggestedBufferSize=" + dwSuggestedBufferSize);
IJ.log(" dwWidth=" + dwWidth);
IJ.log(" dwHeight=" + dwHeight);
}
}
void readStreamHeader() throws Exception, IOException {
fccStreamHandler = readInt();
dwStreamFlags = readInt();
dwPriorityLanguage = readInt();
dwStreamInitialFrames = readInt();
dwStreamScale = readInt();
dwStreamRate = readInt();
dwStreamStart = readInt();
dwStreamLength = readInt();
dwStreamSuggestedBufferSize = readInt();
dwStreamQuality = readInt();
dwStreamSampleSize = readInt();
//rcFrame rectangle follows, ignored
if (verbose) {
IJ.log("VIDEO STREAM HEADER (strh):");
IJ.log(" fccStreamHandler='" + fourccString(fccStreamHandler)+"'");
IJ.log(" dwStreamFlags=" + dwStreamFlags);
IJ.log(" wPriority,wLanguage=" + dwPriorityLanguage);
IJ.log(" dwStreamInitialFrames=" + dwStreamInitialFrames);
IJ.log(" dwStreamScale=" + dwStreamScale);
IJ.log(" dwStreamRate=" + dwStreamRate);
IJ.log(" dwStreamStart=" + dwStreamStart);
IJ.log(" dwStreamLength=" + dwStreamLength);
IJ.log(" dwStreamSuggestedBufferSize=" + dwStreamSuggestedBufferSize);
IJ.log(" dwStreamQuality=" + dwStreamQuality);
IJ.log(" dwStreamSampleSize=" + dwStreamSampleSize);
}
if (dwStreamSampleSize > 1)
throw new Exception("Video stream with "+dwStreamSampleSize+" (more than 1) frames/chunk not supported");
}
/**Read stream format chunk: starts with BitMapInfo, may contain palette
*/
void readBitMapInfo(long endPosition) throws Exception, IOException {
biSize = readInt();
biWidth = readInt();
biHeight = readInt();
biPlanes = readShort();
biBitCount = readShort();
biCompression = readInt();
biSizeImage = readInt();
biXPelsPerMeter = readInt();
biYPelsPerMeter = readInt();
biClrUsed = readInt();
biClrImportant = readInt();
if (verbose) {
IJ.log(" biSize=" + biSize);
IJ.log(" biWidth=" + biWidth);
IJ.log(" biHeight=" + biHeight);
IJ.log(" biPlanes=" + biPlanes);
IJ.log(" biBitCount=" + biBitCount);
IJ.log(" biCompression=0x" + Integer.toHexString(biCompression)+" '"+fourccString(biCompression)+"'");
IJ.log(" biSizeImage=" + biSizeImage);
IJ.log(" biXPelsPerMeter=" + biXPelsPerMeter);
IJ.log(" biYPelsPerMeter=" + biYPelsPerMeter);
IJ.log(" biClrUsed=" + biClrUsed);
IJ.log(" biClrImportant=" + biClrImportant);
}
int allowedBitCount = 0;
boolean readPalette = false;
switch (biCompression) {
case NO_COMPRESSION:
case NO_COMPRESSION_RGB:
case NO_COMPRESSION_RAW:
dataCompression = NO_COMPRESSION;
dataTopDown = biHeight<0; //RGB mode is usually bottom-up, negative height signals top-down
allowedBitCount = 8 | BITMASK24 | 32; //we don't support 1, 2 and 4 byte data
readPalette = biBitCount <= 8;
break;
case NO_COMPRESSION_Y8:
case NO_COMPRESSION_GREY:
case NO_COMPRESSION_Y800:
dataTopDown = true;
dataCompression = NO_COMPRESSION;
allowedBitCount = 8;
break;
case NO_COMPRESSION_Y16:
case NO_COMPRESSION_MIL:
dataCompression = NO_COMPRESSION;
allowedBitCount = 16;
break;
case AYUV_COMPRESSION:
dataCompression = AYUV_COMPRESSION;
allowedBitCount = 32;
break;
case UYVY_COMPRESSION:
case UYNV_COMPRESSION:
dataTopDown = true;
case CYUV_COMPRESSION: //same, not top-down
case V422_COMPRESSION:
dataCompression = UYVY_COMPRESSION;
allowedBitCount = 16;
break;
case YUY2_COMPRESSION:
case YUNV_COMPRESSION:
case YUYV_COMPRESSION:
dataTopDown = true;
dataCompression = YUY2_COMPRESSION;
allowedBitCount = 16;
break;
case YVYU_COMPRESSION:
dataTopDown = true;
dataCompression = YVYU_COMPRESSION;
allowedBitCount = 16;
break;
case JPEG_COMPRESSION:
case JPEG_COMPRESSION2:
case JPEG_COMPRESSION3:
case MJPG_COMPRESSION:
dataCompression = JPEG_COMPRESSION;
variableLength = true;
break;
case PNG_COMPRESSION:
case PNG_COMPRESSION2:
case PNG_COMPRESSION3:
variableLength = true;
dataCompression = PNG_COMPRESSION;
break;
default:
throw new Exception("Unsupported compression: "+Integer.toHexString(biCompression)+
(biCompression>=0x20202020 ? " '" + fourccString(biCompression)+"'" : ""));
}
int bitCountTest = biBitCount==24 ? BITMASK24 : biBitCount; //convert "24" to a flag
if (allowedBitCount!=0 && (bitCountTest & allowedBitCount)==0)
throw new Exception("Unsupported: "+biBitCount+" bits/pixel for compression '"+
fourccString(biCompression)+"'");
if (biHeight < 0) //negative height was for top-down data in RGB mode
biHeight = -biHeight;
// Scan line is padded with zeroes to be a multiple of four bytes
scanLineSize = ((biWidth * biBitCount + 31) / 32) * 4;
// a value of biClrUsed=0 means we determine this based on the bits per pixel
if (readPalette && biClrUsed==0)
biClrUsed = 1 << biBitCount;
if (verbose) {
IJ.log(" > data compression=0x" + Integer.toHexString(dataCompression)+" '"+fourccString(dataCompression)+"'");
IJ.log(" > palette colors=" + biClrUsed);
IJ.log(" > scan line size=" + scanLineSize);
IJ.log(" > data top down=" + dataTopDown);
}
//read color palette
if (readPalette) {
long spaceForPalette = endPosition-raFile.getFilePointer();
if (verbose)
IJ.log(" Reading "+biClrUsed+" Palette colors: " + posSizeString(spaceForPalette));
if (spaceForPalette < biClrUsed*4)
throw new Exception("Not enough data ("+spaceForPalette+") for palette of size "+(biClrUsed*4));
byte[] pr = new byte[biClrUsed];
byte[] pg = new byte[biClrUsed];
byte[] pb = new byte[biClrUsed];
for (int i = 0; i < biClrUsed; i++) {
pb[i] = raFile.readByte();
pg[i] = raFile.readByte();
pr[i] = raFile.readByte();
raFile.readByte();
}
cm = new IndexColorModel(biBitCount, biClrUsed, pr, pg, pb);
}
}
/**Read from the 'movi' chunk. Skips audio ('..wb', etc.), 'LIST' 'rec' etc, only reads '..db' or '..dc'*/
void readMovieData(long endPosition) throws Exception, IOException {
if (verbose)
IJ.log("MOVIE DATA "+posSizeString(endPosition-raFile.getFilePointer())+timeString());
//prepare for reading
int type0xdb = FOURCC_00db + (streamNumber<<8); //'01db' for stream 1, etc. (inverse byte order!)
int type0xdc = FOURCC_00dc + (streamNumber<<8); //'01dc' for stream 1, etc.
if (verbose)
IJ.log("Searching for stream "+streamNumber+": '"+
fourccString(type0xdb)+"' or '"+fourccString(type0xdc)+"' chunks");
int lastFrameToRead = Integer.MAX_VALUE;
if (lastFrameNumber > 0) //last frame number to read: from Dialog
lastFrameToRead = lastFrameNumber;
if (lastFrameNumber < 0 && dwTotalFrames > 0) //negative means "end frame minus ..."
lastFrameToRead = dwTotalFrames+lastFrameNumber;
if (isVirtual)
frameInfos = new Vector(100); //holds frame positions in file (for non-constant frame sizes, should hold long[] with pos and size)
else
stack = new ImageStack(dwWidth, biHeight);
int frameNumber = 1;
while (true) { //loop over all chunks
int type = readType(endPosition);
if (type==0) break; //end of 'movi' reached?
long size = readInt() & SIZE_MASK;
long pos = raFile.getFilePointer();
long nextPos = pos + size;
if ((type==type0xdb || type==type0xdc) && size>0) {
updateProgress();
if (verbose)
IJ.log("movie data '"+fourccString(type)+"' "+posSizeString(size)+timeString());
if (frameNumber >= firstFrameNumber) {
if (isVirtual)
frameInfos.add(new long[]{pos, size, frameNumber*dwMicroSecPerFrame});
else { //read the frame
Object pixels = readFrame(raFile, pos, (int)size);
String label = frameLabel(frameNumber*dwMicroSecPerFrame);
stack.addSlice(label, pixels);
}
}
frameNumber++;
if (frameNumber>lastFrameToRead) break;
} else if (verbose)
IJ.log("skipped '"+fourccString(type)+"' "+posSizeString(size));
if (nextPos > endPosition) break;
raFile.seek(nextPos);
}
}
/** Reads a frame at a given position in the file, returns pixels array */
private Object readFrame (RandomAccessFile rFile, long filePos, int size)
throws Exception, IOException {
rFile.seek(filePos);
if (variableLength) //JPEG or PNG-compressed frames
return readCompressedFrame(rFile, size);
else
return readFixedLengthFrame(rFile, size);
}
/** Reads a JPEG or PNG-compressed frame from a RandomAccessFile and
* returns the pixels array of the resulting image abd sets the
* ColorModel cm (if appropriate) */
private Object readCompressedFrame (RandomAccessFile rFile, int size)
throws Exception, IOException {
InputStream inputStream = new raInputStream(rFile, size, biCompression==MJPG_COMPRESSION);
BufferedImage bi = ImageIO.read(inputStream);
int type = bi.getType();
ImageProcessor ip = null;
//IJ.log("BufferedImage Type="+type);
if (type==BufferedImage.TYPE_BYTE_GRAY) {
ip = new ByteProcessor(bi);
} else if (type==bi.TYPE_BYTE_INDEXED) {
cm = bi.getColorModel();
ip = new ByteProcessor((Image)bi);
} else
ip = new ColorProcessor(bi);
if (convertToGray)
ip = ip.convertToByte(false);
if (flipVertical)
ip.flipVertical();
return ip.getPixels();
}
/** Read a fixed-length frame (RandomAccessFile rFile, long filePos, int size)
* return the pixels array of the resulting image
*/
private Object readFixedLengthFrame (RandomAccessFile rFile, int size) throws Exception, IOException {
if (size < scanLineSize*biHeight)
throw new Exception("Data chunk size "+size+" too short ("+(scanLineSize*biHeight)+" required)");
byte[] rawData = new byte[size];
int n = rFile.read(rawData, 0, size);
if (n < rawData.length)
throw new Exception("Frame ended prematurely after " + n + " bytes");
boolean topDown = flipVertical ? !dataTopDown : dataTopDown;
Object pixels = null;
byte[] bPixels = null;
int[] cPixels = null;
short[] sPixels = null;
if (biBitCount <=8 || convertToGray) {
bPixels = new byte[dwWidth * biHeight];
pixels = bPixels;
} else if (biBitCount == 16 && dataCompression == NO_COMPRESSION) {
sPixels = new short[dwWidth * biHeight];
pixels = sPixels;
} else {
cPixels = new int[dwWidth * biHeight];
pixels = cPixels;
}
int offset = topDown ? 0 : (biHeight-1)*dwWidth;
int rawOffset = 0;
for (int i = biHeight - 1; i >= 0; i--) { //for all lines
if (biBitCount <=8)
unpack8bit(rawData, rawOffset, bPixels, offset, dwWidth);
else if (convertToGray)
unpackGray(rawData, rawOffset, bPixels, offset, dwWidth);
else if (biBitCount==16 && dataCompression == NO_COMPRESSION)
unpackShort(rawData, rawOffset, sPixels, offset, dwWidth);
else
unpack(rawData, rawOffset, cPixels, offset, dwWidth);
rawOffset += scanLineSize;
offset += topDown ? dwWidth : -dwWidth;
}
return pixels;
}
/** For one line: copy byte data into the byte array for creating a ByteProcessor */
void unpack8bit(byte[] rawData, int rawOffset, byte[] pixels, int byteOffset, int w) {
for (int i = 0; i < w; i++)
pixels[byteOffset + i] = rawData[rawOffset + i];
}
/** For one line: Unpack and convert YUV or RGB video data to grayscale (byte array for ByteProcessor) */
void unpackGray(byte[] rawData, int rawOffset, byte[] pixels, int byteOffset, int w) {
int j = byteOffset;
int k = rawOffset;
if (dataCompression == 0) {
for (int i = 0; i < w; i++) {
int b0 = (((int) (rawData[k++])) & 0xff);
int b1 = (((int) (rawData[k++])) & 0xff);
int b2 = (((int) (rawData[k++])) & 0xff);
if (biBitCount==32) k++; // ignore 4th byte (alpha value)
pixels[j++] = (byte)((b0*934 + b1*4809 + b2*2449 + 4096)>>13); //0.299*R+0.587*G+0.114*B
}
} else {
if (dataCompression==UYVY_COMPRESSION || dataCompression==AYUV_COMPRESSION)
k++; //skip first byte in these formats (chroma)
int step = dataCompression==AYUV_COMPRESSION ? 4 : 2;
for (int i = 0; i < w; i++) {
pixels[j++] = rawData[k]; //Non-standard: no scaling from 16-235 to 0-255 here
k+=step;
}
}
}
/** For one line: Unpack 16bit grayscale data and convert to short array for ShortProcessor */
void unpackShort(byte[] rawData, int rawOffset, short[] pixels, int shortOffset, int w) {
int j = shortOffset;
int k = rawOffset;
for (int i = 0; i < w; i++) {
pixels[j++] = (short) ((int)(rawData[k++] & 0xFF)| (((int)(rawData[k++] & 0xFF))<<8));
}
}
/** For one line: Read YUV, RGB or RGB+alpha data and writes RGB int array for ColorProcessor */
void unpack(byte[] rawData, int rawOffset, int[] pixels, int intOffset, int w) {
int j = intOffset;
int k = rawOffset;
switch (dataCompression) {
case NO_COMPRESSION:
for (int i = 0; i < w; i++) {
int b0 = (((int) (rawData[k++])) & 0xff);
int b1 = (((int) (rawData[k++])) & 0xff) << 8;
int b2 = (((int) (rawData[k++])) & 0xff) << 16;
if (biBitCount==32) k++; // ignore 4th byte (alpha value)
pixels[j++] = 0xff000000 | b0 | b1 | b2;
}
break;
case YUY2_COMPRESSION:
for (int i = 0; i < w/2; i++) {
int y0 = rawData[k++] & 0xff;
int u = rawData[k++] ^ 0xffffff80; //converts byte range 0...ff to -128 ... 127
int y1 = rawData[k++] & 0xff;
int v = rawData[k++] ^ 0xffffff80;
writeRGBfromYUV(y0, u, v, pixels, j++);
writeRGBfromYUV(y1, u, v, pixels, j++);
}
break;
case UYVY_COMPRESSION:
for (int i = 0; i < w/2; i++) {
int u = rawData[k++] ^ 0xffffff80;
int y0 = rawData[k++] & 0xff;
int v = rawData[k++] ^ 0xffffff80;
int y1 = rawData[k++] & 0xff;
writeRGBfromYUV(y0, u, v, pixels, j++);
writeRGBfromYUV(y1, u, v, pixels, j++);
}
break;
case YVYU_COMPRESSION:
for (int i = 0; i < w/2; i++) {
int y0 = rawData[k++] & 0xff;
int v = rawData[k++] ^ 0xffffff80;
int y1 = rawData[k++] & 0xff;
int u = rawData[k++] ^ 0xffffff80;
writeRGBfromYUV(y0, u, v, pixels, j++);
writeRGBfromYUV(y1, u, v, pixels, j++);
}
break;
case AYUV_COMPRESSION:
for (int i = 0; i < w; i++) {
k++; //ignore alpha channel
int y = rawData[k++] & 0xff;
int v = rawData[k++] ^ 0xffffff80;
int u = rawData[k++] ^ 0xffffff80;
writeRGBfromYUV(y, u, v, pixels, j++);
}
break;
}
}
/** Write an intData RGB value converted from YUV,
* The y range between 16 and 235 becomes 0...255
* u, v should be between -112 and +112
*/
void writeRGBfromYUV(int y, int u, int v, int[]pixels, int intArrayIndex) {
//int r = (int)(1.164*(y-16)+1.596*v+0.5);
//int g = (int)(1.164*(y-16)-0.391*u-0.813*v+0.5);
//int b = (int)(1.164*(y-16)+2.018*u+0.5);
int r = (9535*y + 13074*v -148464) >> 13;
int g = (9535*y - 6660*v - 3203*u -148464) >> 13;
int b = (9535*y + 16531*u -148464) >> 13;
if (r>255) r=255; if (r<0) r=0;
if (g>255) g=255; if (g<0) g=0;
if (b>255) b=255; if (b<0) b=0;
pixels[intArrayIndex] = 0xff000000 | (r<<16) | (g<<8) | b;
}
/** Read 4-byte int with Intel (little-endian) byte order
* (note: RandomAccessFile.readInt has other byte order than AVI) */
int readInt() throws IOException {
int result = 0;
for (int shiftBy = 0; shiftBy < 32; shiftBy += 8)
result |= (raFile.readByte() & 0xff) << shiftBy;
return result;
}
/** Read 2-byte short with Intel (little-endian) byte order
* (note: RandomAccessFile.readShort has other byte order than AVI) */
short readShort() throws IOException {
int low = raFile.readByte() & 0xff;
int high = raFile.readByte() & 0xff;
return (short) (high << 8 | low);
}
/** Read type of next chunk that is not JUNK.
* Returns type (or 0 if no non-JUNK chunk until endPosition) */
private int readType(long endPosition) throws IOException {
while (true) {
long pos = raFile.getFilePointer();
if (pos%paddingGranularity!=0) {
pos = (pos/paddingGranularity+1)*paddingGranularity;
raFile.seek(pos); //pad to even address
}
if (pos >= endPosition) return 0;
int type = readInt();
if (type != FOURCC_JUNK)
return type;
long size = readInt()&SIZE_MASK;
if (verbose)
IJ.log("Skip JUNK: "+posSizeString(size));
raFile.seek(raFile.getFilePointer()+size); //skip junk
}
}
private void setFramesPerSecond (ImagePlus imp) {
if (dwMicroSecPerFrame<1000 && dwStreamRate>0) //if no reasonable frame time, get it from rate
dwMicroSecPerFrame = (int)(dwStreamScale*1e6/dwStreamRate);
if (dwMicroSecPerFrame>=1000)
imp.getCalibration().fps = 1e6 / dwMicroSecPerFrame;
}
private String frameLabel(long timeMicroSec) {
return IJ.d2s(timeMicroSec/1.e6)+" s";
}
private String posSizeString(long size) throws IOException {
return posSizeString(raFile.getFilePointer(), size);
}
private String posSizeString(long pos, long size) throws IOException {
return "0x"+Long.toHexString(pos)+"-0x"+Long.toHexString(pos+size-1)+" ("+size+" Bytes)";
}
private String timeString() {
return " (t="+(System.currentTimeMillis()-startTime)+" ms)";
}
/** returns a string of a four-cc code corresponding to an int (Intel byte order) */
private String fourccString(int fourcc) {
String s = "";
for (int i=0; i<4; i++) {
int c = fourcc&0xff;
s += Character.toString((char)c);
fourcc >>= 8;
}
return s;
}
private String exceptionMessage (Exception e) {
String msg;
if (e.getClass() == Exception.class) //for "home-built" exceptions: message only
msg = e.getMessage();
else
msg = e + "\n" + e.getStackTrace()[0]+"\n"+e.getStackTrace()[1];
if (msg.indexOf("Huffman table")!=-1)
return "Cannot open M_JPEG AVIs that are missing Huffman tables";
else
return "An error occurred reading the file.\n \n" + msg;
}
void updateProgress() throws IOException {
IJ.showProgress((double) raFile.getFilePointer() / fileSize);
}
/** An input stream reading from a RandomAccessFile (starting at the current position)
* This class contains a hack to convert a 'Define Huffman Table' (DHT) segments
* from MJPG to JPEG.
* The hack works only if the first bytes are read in a sufficiently large
* array to include the start of the DHT segment.
*/
class raInputStream extends InputStream {
RandomAccessFile rFile; //where to read the data from
int readableSize; //number of bytes that one should expect to be readable
boolean fixMJPG; //whether to use an ugly hack to convert MJPG frames to JPEG
/** Constructor */
raInputStream (RandomAccessFile rFile, int readableSize, boolean fixMJPG) {
this.rFile = rFile;
this.readableSize = readableSize;
this.fixMJPG = fixMJPG;
}
public int available () {
return readableSize;
}
// Read methods.
// Reading beyond start position + readableSize (i.e. beyond the frame in the avi file)
// is possible - no checks are done (as long as it is within the bounds of the file).
public int read () throws IOException {
fixMJPG = false; //the fix won't work after reading single bytes
return rFile.read();
}
public int read (byte[] b) throws IOException {
return read(b, 0, b.length);
}
public int read (byte[] b, int off, int len) throws IOException {
int nBytes = rFile.read(b, off, len);
if (fixMJPG) {
doFixMJPG(b, nBytes);
fixMJPG = false;
}
return nBytes;
}
// A Hack: Replace 0xffb3 segment markers with 0xffc4, the JPEG code for
// 'Define Huffman Table' (DHT)
// This makes at least some MJPG files readable by the JPEG decoder
private void doFixMJPG (byte[] b, int len) {
//IJ.log("data code="+Integer.toHexString(readShort(b, 0)));
if (readShort(b, 0)!=0xffd8 || len<6) return; //not a start of JPEG-like data
int offset = 2;
while (true) {
int code = readShort(b, offset); //read segment type
if (code==0xffda || code==0xffd9) return; //start of image data or end
if (code==0xffe2) {
b[offset+1] = (byte)0xc4;
return; //fixed
}
offset += 2;
int segmentLength = readShort(b, offset);
offset += segmentLength;
if (offset>len-4 || segmentLength<0) return;//pointed out of file
}
}
private int readShort(byte[] b, int offset) {
return ((b[offset]&0xff)<<8) | (b[offset+1]&0xff);
}
}
}