//
// ImageConverter.java
//
/*
OME Bio-Formats package for reading and converting biological file formats.
Copyright (C) 2005-@year@ UW-Madison LOCI and Glencoe Software, Inc.
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, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package loci.formats.tools;
import java.awt.image.IndexColorModel;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.io.IOException;
import java.util.List;
import loci.common.DataTools;
import loci.common.DebugTools;
import loci.common.Location;
import loci.common.services.DependencyException;
import loci.common.services.ServiceException;
import loci.common.services.ServiceFactory;
import loci.formats.ChannelFiller;
import loci.formats.ChannelMerger;
import loci.formats.ChannelSeparator;
import loci.formats.FilePattern;
import loci.formats.FileStitcher;
import loci.formats.FormatException;
import loci.formats.FormatTools;
import loci.formats.IFormatReader;
import loci.formats.IFormatWriter;
import loci.formats.ImageReader;
import loci.formats.ImageTools;
import loci.formats.ImageWriter;
import loci.formats.MetadataTools;
import loci.formats.MinMaxCalculator;
import loci.formats.MissingLibraryException;
import loci.formats.ReaderWrapper;
import loci.formats.in.OMETiffReader;
import loci.formats.meta.IMetadata;
import loci.formats.meta.MetadataRetrieve;
import loci.formats.meta.MetadataStore;
import loci.formats.out.TiffWriter;
import loci.formats.services.OMEXMLService;
import loci.formats.services.OMEXMLServiceImpl;
import loci.formats.tiff.IFD;
import ome.xml.model.Image;
import ome.xml.model.OME;
import ome.xml.model.enums.PixelType;
import ome.xml.model.primitives.PositiveInteger;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* ImageConverter is a utility class for converting a file between formats.
*
* <dl><dt><b>Source code:</b></dt>
* <dd><a href="http://trac.openmicroscopy.org.uk/ome/browser/bioformats.git/components/bio-formats/src/loci/formats/tools/ImageConverter.java">Trac</a>,
* <a href="http://git.openmicroscopy.org/?p=bioformats.git;a=blob;f=components/bio-formats/src/loci/formats/tools/ImageConverter.java;hb=HEAD">Gitweb</a></dd></dl>
*/
public final class ImageConverter {
// -- Constants --
private static final Logger LOGGER =
LoggerFactory.getLogger(ImageConverter.class);
// -- Fields --
private String in = null, out = null;
private String map = null;
private String compression = null;
private boolean stitch = false, separate = false, merge = false, fill = false;
private boolean bigtiff = false, group = true;
private boolean printVersion = false;
private boolean autoscale = false;
private Boolean overwrite = null;
private int series = -1;
private int firstPlane = 0;
private int lastPlane = Integer.MAX_VALUE;
private int channel = -1, zSection = -1, timepoint = -1;
private int xCoordinate = 0, yCoordinate = 0, width = 0, height = 0;
private IFormatReader reader;
private MinMaxCalculator minMax;
// -- Constructor --
private ImageConverter() { }
// -- Utility methods --
/** A utility method for converting a file from the command line. */
public boolean testConvert(IFormatWriter writer, String[] args)
throws FormatException, IOException
{
DebugTools.enableLogging("INFO");
if (args != null) {
for (int i=0; i<args.length; i++) {
if (args[i].startsWith("-") && args.length > 1) {
if (args[i].equals("-debug")) {
DebugTools.enableLogging("DEBUG");
}
else if (args[i].equals("-stitch")) stitch = true;
else if (args[i].equals("-separate")) separate = true;
else if (args[i].equals("-merge")) merge = true;
else if (args[i].equals("-expand")) fill = true;
else if (args[i].equals("-bigtiff")) bigtiff = true;
else if (args[i].equals("-map")) map = args[++i];
else if (args[i].equals("-compression")) compression = args[++i];
else if (args[i].equals("-nogroup")) group = false;
else if (args[i].equals("-autoscale")) autoscale = true;
else if (args[i].equals("-overwrite")) {
overwrite = true;
}
else if (args[i].equals("-nooverwrite")) {
overwrite = false;
}
else if (args[i].equals("-channel")) {
channel = Integer.parseInt(args[++i]);
}
else if (args[i].equals("-z")) {
zSection = Integer.parseInt(args[++i]);
}
else if (args[i].equals("-timepoint")) {
timepoint = Integer.parseInt(args[++i]);
}
else if (args[i].equals("-series")) {
try {
series = Integer.parseInt(args[++i]);
}
catch (NumberFormatException exc) { }
}
else if (args[i].equals("-range")) {
try {
firstPlane = Integer.parseInt(args[++i]);
lastPlane = Integer.parseInt(args[++i]) + 1;
}
catch (NumberFormatException exc) { }
}
else if (args[i].equals("-crop")) {
String[] tokens = args[++i].split(",");
xCoordinate = Integer.parseInt(tokens[0]);
yCoordinate = Integer.parseInt(tokens[1]);
width = Integer.parseInt(tokens[2]);
height = Integer.parseInt(tokens[3]);
}
else {
LOGGER.error("Found unknown command flag: {}; exiting.", args[i]);
return false;
}
}
else {
if (args[i].equals("-version")) printVersion = true;
else if (in == null) in = args[i];
else if (out == null) out = args[i];
else {
LOGGER.error("Found unknown argument: {}; exiting.", args[i]);
LOGGER.error("You should specify exactly one input file and " +
"exactly one output file.");
return false;
}
}
}
}
if (printVersion) {
LOGGER.info("Version: {}", FormatTools.VERSION);
LOGGER.info("VCS revision: {}", FormatTools.VCS_REVISION);
LOGGER.info("Build date: {}", FormatTools.DATE);
return true;
}
if (in == null || out == null) {
String[] s = {
"To convert a file between formats, run:",
" bfconvert [-debug] [-stitch] [-separate] [-merge] [-expand]",
" [-bigtiff] [-compression codec] [-series series] [-map id]",
" [-range start end] [-crop x,y,w,h] [-channel channel] [-z Z]",
" [-timepoint timepoint] [-nogroup] [-autoscale] [-version]",
" in_file out_file",
"",
" -version: print the library version and exit",
" -debug: turn on debugging output",
" -stitch: stitch input files with similar names",
" -separate: split RGB images into separate channels",
" -merge: combine separate channels into RGB image",
" -expand: expand indexed color to RGB",
" -bigtiff: force BigTIFF files to be written",
"-compression: specify the codec to use when saving images",
" -series: specify which image series to convert",
" -map: specify file on disk to which name should be mapped",
" -range: specify range of planes to convert (inclusive)",
" -nogroup: force multi-file datasets to be read as individual" +
" files",
" -autoscale: automatically adjust brightness and contrast before",
" converting; this may mean that the original pixel",
" values are not preserved",
" -overwrite: always overwrite the output file, if it already exists",
"-nooverwrite: never overwrite the output file, if it already exists",
" -crop: crop images before converting; argument is 'x,y,w,h'",
" -channel: only convert the specified channel (indexed from 0)",
" -z: only convert the specified Z section (indexed from 0)",
" -timepoint: only convert the specified timepoint (indexed from 0)",
"",
"If any of the following patterns are present in out_file, they will",
"be replaced with the indicated metadata value from the input file.",
"",
" Pattern:\tMetadata value:",
" ---------------------------",
" " + FormatTools.SERIES_NUM + "\t\tseries index",
" " + FormatTools.SERIES_NAME + "\t\tseries name",
" " + FormatTools.CHANNEL_NUM + "\t\tchannel index",
" " + FormatTools.CHANNEL_NAME +"\t\tchannel name",
" " + FormatTools.Z_NUM + "\t\tZ index",
" " + FormatTools.T_NUM + "\t\tT index",
" " + FormatTools.TIMESTAMP + "\t\tacquisition timestamp",
"",
"If any of these patterns are present, then the images to be saved",
"will be split into multiple files. For example, if the input file",
"contains 5 Z sections and 3 timepoints, and out_file is",
"",
" converted_Z" + FormatTools.Z_NUM + "_T" +
FormatTools.T_NUM + ".tiff",
"",
"then 15 files will be created, with the names",
"",
" converted_Z0_T0.tiff",
" converted_Z0_T1.tiff",
" converted_Z0_T2.tiff",
" converted_Z1_T0.tiff",
" ...",
" converted_Z4_T2.tiff",
"",
"Each file would have a single image plane."
};
for (int i=0; i<s.length; i++) LOGGER.info(s[i]);
return false;
}
if (new Location(out).exists()) {
if (overwrite == null) {
LOGGER.warn("Output file {} exists.", out);
LOGGER.warn("Do you want to overwrite it? ([y]/n)");
BufferedReader r = new BufferedReader(new InputStreamReader(System.in));
String choice = r.readLine().trim().toLowerCase();
overwrite = !choice.startsWith("n");
}
if (!overwrite) {
LOGGER.warn("Exiting; next time, please specify an output file that " +
"does not exist.");
return false;
}
else {
new Location(out).delete();
}
}
if (map != null) Location.mapId(in, map);
long start = System.currentTimeMillis();
LOGGER.info(in);
reader = new ImageReader();
if (stitch) {
reader = new FileStitcher(reader);
Location f = new Location(in);
String pat = null;
if (!f.exists()) {
pat = in;
}
else {
pat = FilePattern.findPattern(f);
}
if (pat != null) in = pat;
}
if (separate) reader = new ChannelSeparator(reader);
if (merge) reader = new ChannelMerger(reader);
if (fill) reader = new ChannelFiller(reader);
minMax = null;
if (autoscale) {
reader = new MinMaxCalculator(reader);
minMax = (MinMaxCalculator) reader;
}
reader.setGroupFiles(group);
reader.setMetadataFiltered(true);
reader.setOriginalMetadataPopulated(true);
OMEXMLService service = null;
try {
ServiceFactory factory = new ServiceFactory();
service = factory.getInstance(OMEXMLService.class);
reader.setMetadataStore(service.createOMEXMLMetadata());
}
catch (DependencyException de) {
throw new MissingLibraryException(OMEXMLServiceImpl.NO_OME_XML_MSG, de);
}
catch (ServiceException se) {
throw new FormatException(se);
}
reader.setId(in);
MetadataStore store = reader.getMetadataStore();
MetadataTools.populatePixels(store, reader, false, false);
boolean dimensionsSet = true;
if (width == 0 || height == 0) {
width = reader.getSizeX();
height = reader.getSizeY();
dimensionsSet = false;
}
if (store instanceof MetadataRetrieve) {
if (series >= 0) {
try {
String xml = service.getOMEXML(service.asRetrieve(store));
OME root = (OME) store.getRoot();
Image exportImage = root.getImage(series);
IMetadata meta = service.createOMEXMLMetadata(xml);
OME newRoot = (OME) meta.getRoot();
while (newRoot.sizeOfImageList() > 0) {
newRoot.removeImage(newRoot.getImage(0));
}
newRoot.addImage(exportImage);
meta.setRoot(newRoot);
meta.setPixelsSizeX(new PositiveInteger(width), 0);
meta.setPixelsSizeY(new PositiveInteger(height), 0);
if (autoscale) {
store.setPixelsType(PixelType.UINT8, 0);
}
writer.setMetadataRetrieve((MetadataRetrieve) meta);
}
catch (ServiceException e) {
throw new FormatException(e);
}
}
else {
for (int i=0; i<reader.getSeriesCount(); i++) {
if (width != reader.getSizeX() || height != reader.getSizeY()) {
store.setPixelsSizeX(new PositiveInteger(width), 0);
store.setPixelsSizeY(new PositiveInteger(height), 0);
}
if (autoscale) {
store.setPixelsType(PixelType.UINT8, i);
}
}
writer.setMetadataRetrieve((MetadataRetrieve) store);
}
}
writer.setWriteSequentially(true);
if (writer instanceof TiffWriter) {
((TiffWriter) writer).setBigTiff(bigtiff);
}
else if (writer instanceof ImageWriter) {
IFormatWriter w = ((ImageWriter) writer).getWriter(out);
if (w instanceof TiffWriter) {
((TiffWriter) w).setBigTiff(bigtiff);
}
}
String format = writer.getFormat();
LOGGER.info("[{}] -> {} [{}]",
new Object[] {reader.getFormat(), out, format});
long mid = System.currentTimeMillis();
int total = 0;
int num = writer.canDoStacks() ? reader.getSeriesCount() : 1;
long read = 0, write = 0;
int first = series == -1 ? 0 : series;
int last = series == -1 ? num : series + 1;
long timeLastLogged = System.currentTimeMillis();
for (int q=first; q<last; q++) {
reader.setSeries(q);
if (!dimensionsSet) {
width = reader.getSizeX();
height = reader.getSizeY();
}
int writerSeries = series == -1 ? q : 0;
writer.setSeries(writerSeries);
writer.setInterleaved(reader.isInterleaved() && !autoscale);
writer.setValidBitsPerPixel(reader.getBitsPerPixel());
int numImages = writer.canDoStacks() ? reader.getImageCount() : 1;
int startPlane = (int) Math.max(0, firstPlane);
int endPlane = (int) Math.min(numImages, lastPlane);
numImages = endPlane - startPlane;
if (channel >= 0) {
numImages /= reader.getEffectiveSizeC();
}
if (zSection >= 0) {
numImages /= reader.getSizeZ();
}
if (timepoint >= 0) {
numImages /= reader.getSizeT();
}
total += numImages;
int count = 0;
for (int i=startPlane; i<endPlane; i++) {
int[] coords = reader.getZCTCoords(i);
if ((zSection >= 0 && coords[0] != zSection) || (channel >= 0 &&
coords[1] != channel) || (timepoint >= 0 && coords[2] != timepoint))
{
continue;
}
writer.setId(FormatTools.getFilename(q, i, reader, out));
if (compression != null) writer.setCompression(compression);
long s = System.currentTimeMillis();
long m = convertPlane(writer, i, startPlane);
long e = System.currentTimeMillis();
read += m - s;
write += e - m;
// log number of planes processed every second or so
if (count == numImages - 1 || (e - timeLastLogged) / 1000 > 0) {
int current = (count - startPlane) + 1;
int percent = 100 * current / numImages;
StringBuilder sb = new StringBuilder();
sb.append("\t");
int numSeries = last - first;
if (numSeries > 1) {
sb.append("Series ");
sb.append(q);
sb.append(": converted ");
}
else sb.append("Converted ");
LOGGER.info(sb.toString() + "{}/{} planes ({}%)",
new Object[] {current, numImages, percent});
timeLastLogged = e;
}
count++;
}
}
writer.close();
long end = System.currentTimeMillis();
LOGGER.info("[done]");
// output timing results
float sec = (end - start) / 1000f;
long initial = mid - start;
float readAvg = (float) read / total;
float writeAvg = (float) write / total;
LOGGER.info("{}s elapsed ({}+{}ms per plane, {}ms overhead)",
new Object[] {sec, readAvg, writeAvg, initial});
return true;
}
// -- Helper methods --
private long convertPlane(IFormatWriter writer, int index, int startPlane)
throws FormatException, IOException
{
if (width * height >= 4096 * 4096) {
// this is a "big image", so we will attempt to convert it one tile
// at a time
if ((writer instanceof TiffWriter) || ((writer instanceof ImageWriter) &&
(((ImageWriter) writer).getWriter(out) instanceof TiffWriter)))
{
return convertTilePlane(writer, index, startPlane);
}
}
byte[] buf =
reader.openBytes(index, xCoordinate, yCoordinate, width, height);
autoscalePlane(buf, index);
applyLUT(writer);
long m = System.currentTimeMillis();
writer.saveBytes(index - startPlane, buf);
return m;
}
private long convertTilePlane(IFormatWriter writer, int index, int startPlane)
throws FormatException, IOException
{
int w = width;
int h = 1;
int nXTiles = width / w;
int nYTiles = height / h;
IFD ifd = new IFD();
Long m = null;
for (int y=0; y<nYTiles; y++) {
for (int x=0; x<nXTiles; x++) {
int tileX = xCoordinate + x * w;
int tileY = yCoordinate + y * h;
int tileWidth = x < nXTiles - 1 ? w : w - (width % w);
int tileHeight = y < nYTiles - 1 ? h : h - (height % h);
byte[] buf =
reader.openBytes(index, tileX, tileY, tileWidth, tileHeight);
autoscalePlane(buf, index);
applyLUT(writer);
if (m == null) {
m = System.currentTimeMillis();
}
ifd.put(IFD.TILE_WIDTH, tileWidth);
ifd.put(IFD.TILE_LENGTH, tileHeight);
if (writer instanceof TiffWriter) {
((TiffWriter) writer).saveBytes(index - startPlane, buf,
ifd, tileX, tileY, tileWidth, tileHeight);
}
else if (writer instanceof ImageWriter) {
IFormatWriter baseWriter = ((ImageWriter) writer).getWriter(out);
if (baseWriter instanceof TiffWriter) {
((TiffWriter) baseWriter).saveBytes(index - startPlane, buf, ifd,
tileX, tileY, tileWidth, tileHeight);
}
}
}
}
return m;
}
private void autoscalePlane(byte[] buf, int index)
throws FormatException, IOException
{
if (autoscale) {
Double min = null;
Double max = null;
Double[] planeMin = minMax.getPlaneMinimum(index);
Double[] planeMax = minMax.getPlaneMaximum(index);
if (planeMin != null && planeMax != null) {
min = planeMin[0];
max = planeMax[0];
for (int j=1; j<planeMin.length; j++) {
if (planeMin[j].doubleValue() < min.doubleValue()) {
min = planeMin[j];
}
if (planeMax[j].doubleValue() < max.doubleValue()) {
max = planeMax[j];
}
}
}
int pixelType = reader.getPixelType();
int bpp = FormatTools.getBytesPerPixel(pixelType);
boolean floatingPoint = FormatTools.isFloatingPoint(pixelType);
Object pix = DataTools.makeDataArray(buf, bpp, floatingPoint,
reader.isLittleEndian());
byte[][] b = ImageTools.make24Bits(pix, width, height,
reader.isInterleaved(), false, min, max);
int channelCount = reader.getRGBChannelCount();
int copyComponents = (int) Math.min(channelCount, b.length);
buf = new byte[channelCount * b[0].length];
for (int j=0; j<copyComponents; j++) {
System.arraycopy(b[j], 0, buf, b[0].length * j, b[0].length);
}
}
}
private void applyLUT(IFormatWriter writer)
throws FormatException, IOException
{
byte[][] lut = reader.get8BitLookupTable();
if (lut != null) {
IndexColorModel model = new IndexColorModel(8, lut[0].length,
lut[0], lut[1], lut[2]);
writer.setColorModel(model);
}
}
// -- Main method --
public static void main(String[] args) throws FormatException, IOException {
ImageConverter converter = new ImageConverter();
if (!converter.testConvert(new ImageWriter(), args)) System.exit(1);
System.exit(0);
}
}