Newer
Older
package cz.it4i.qcmp.cli;
import cz.it4i.qcmp.ScifioWrapper;
import cz.it4i.qcmp.compression.CompressionOptions;
import cz.it4i.qcmp.compression.CompressorDecompressorBase;
import cz.it4i.qcmp.data.HyperStackDimensions;
import cz.it4i.qcmp.data.Range;
import cz.it4i.qcmp.data.V2i;
import cz.it4i.qcmp.data.V3i;
import cz.it4i.qcmp.fileformat.FileExtensions;
import cz.it4i.qcmp.fileformat.QuantizationType;
import cz.it4i.qcmp.io.FileInputData;
import cz.it4i.qcmp.io.InputData;
import io.scif.FormatException;
import io.scif.Plane;
import io.scif.Reader;
import org.apache.commons.io.FilenameUtils;
import java.io.File;
import java.util.Optional;
public class CompressionOptionsCLIParser extends CompressionOptions implements Cloneable {
private static final int DEFAULT_BITS_PER_PIXEL = 8;
/**
* Chosen program method.
*/
/**
* Flag whether parse error occurred.
*/
private boolean parseErrorOccurred;
/**
* Parse error information.
*/
private String parseError;
/**
* Parse provided command line arguments.
*
* @param cmdInput Command line arguments.
*/
public CompressionOptionsCLIParser(final CommandLine cmdInput) {
private String removeQCMPFileExtension(final String originalPath) {
if (originalPath.toUpperCase().endsWith(CompressorDecompressorBase.EXTENSION)) {
return originalPath.substring(0, originalPath.length() - CompressorDecompressorBase.EXTENSION.length());
}
return originalPath;
}
/**
* Creates default output file path depending on the chosen program method.
*
* @param inputPath Input file path.
* @return Default output file path.
private String getDefaultOutputFilePath(final String inputPath) {
// No default output file for custom function.
if (method == ProgramMethod.CustomFunction)
return "";
final File inputFile = new File(inputPath);
final File outputFile = new File(Paths.get("").toAbsolutePath().toString(), inputFile.getName());
// Default value is current directory with input file name.
String defaultValue = outputFile.getAbsolutePath();
switch (method) {
case Compress: {
// Add compressed file extension.
defaultValue += CompressorDecompressorBase.EXTENSION;
}
break;
// If it ends with QCMP file extension remove the extension.
defaultValue = removeQCMPFileExtension(defaultValue);
// Remove the old extension and add RAW extension
defaultValue = defaultValue.replace(FilenameUtils.getExtension(defaultValue),
CompressorDecompressorBase.RAW_EXTENSION_NO_DOT);
break;
case Benchmark: {
defaultValue = new File(inputFile.getParent(), "benchmark").getAbsolutePath();
}
break;
case InspectFile:
defaultValue += ".txt";
break;
case TrainCodebook:
case PrintHelp:
case CustomFunction:
break;
/**
* Parses provided command line arguments to program options.
*
* @param cmd Provided command line arguments.
*/
private void parseCLI(final CommandLine cmd) {
final StringBuilder errorBuilder = new StringBuilder("Errors:\n");
parseErrorOccurred = false;
parseProgramMethod(cmd, errorBuilder);
if (method == ProgramMethod.PrintHelp)
return;
parseCompressionType(cmd, errorBuilder);
parseBitsPerPixel(cmd, errorBuilder);
if (cmd.hasOption(CliConstants.USE_MIDDLE_PLANE_LONG))
setCodebookType(CodebookType.MiddlePlane);
else if (cmd.hasOption(CliConstants.CODEBOOK_CACHE_FOLDER_LONG)) {
final String cbc = cmd.getOptionValue(CliConstants.CODEBOOK_CACHE_FOLDER_LONG, null);
assert (cbc != null);
setCodebookType(CodebookType.Global);
setCodebookCacheFolder(cbc);
} else {
setCodebookType(CodebookType.Individual);
}
final String[] fileInfo = cmd.getArgs();
parseInputFilePart(errorBuilder, fileInfo);
setVerbose(cmd.hasOption(CliConstants.VERBOSE_LONG));
if (cmd.hasOption(CliConstants.WORKER_COUNT_LONG)) {
final String wcString = cmd.getOptionValue(CliConstants.WORKER_COUNT_LONG);
final Optional<Integer> pr = ParseUtils.tryParseInt(wcString);
if (pr.isPresent()) {
setWorkerCount(pr.get());
parseErrorOccurred = true;
errorBuilder.append("Unable to parse worker count. Expected int got: ").append(wcString).append('\n');
}
}
if (!parseErrorOccurred) {
setOutputFilePath(cmd.getOptionValue(CliConstants.OUTPUT_LONG,
getDefaultOutputFilePath(getInputDataInfo().getFilePath())));
setCodebookCacheFolder(cmd.getOptionValue(CliConstants.CODEBOOK_CACHE_FOLDER_LONG, null));
if (getMethod() == ProgramMethod.TrainCodebook) {
setCodebookCacheFolder(getOutputFilePath());
}
parseError = errorBuilder.toString();
/**
* Parse input file info from command line arguments.
*
* @param errorBuilder String error builder.
* @param inputFileArguments Input file info strings.
private void parseInputFilePart(final StringBuilder errorBuilder, final String[] inputFileArguments) {
parseErrorOccurred = true;
errorBuilder.append("Missing input file option");
return;
}
// Decompress and Inspect methods doesn't require additional file information.
if ((method == ProgramMethod.Decompress) || (method == ProgramMethod.InspectFile)) {
setInputDataInfo(new FileInputData(inputFileArguments[0], null));
return;
}
// Check if input file exists.
if (!new File(inputFileArguments[0]).exists()) {
parseErrorOccurred = true;
errorBuilder.append("Input file doesn't exist. Provided path: '").append(inputFileArguments[0]).append("'\n");
final String extension = FilenameUtils.getExtension(inputFileArguments[0]).toLowerCase();
if (FileExtensions.RAW.equals(extension)) {
parseRawFileArguments(errorBuilder, inputFileArguments);
// Default loading through SCIFIO.
parseSCIFIOFileArguments(errorBuilder, inputFileArguments);
private void parseSCIFIOFileArguments(final StringBuilder errorBuilder, final String[] inputFileArguments) {
final Reader reader;
reader = ScifioWrapper.getReader(inputFileArguments[0]);
} catch (final IOException | FormatException e) {
parseErrorOccurred = true;
errorBuilder.append("Failed to get SCIFIO reader for file.\n");
errorBuilder.append(e.getMessage());
return;
}
final int imageCount = reader.getImageCount();
if (imageCount != 1) {
parseErrorOccurred = true;
errorBuilder.append("We are currently not supporting files with multiple images.\n");
return;
}
final long planeCount = reader.getPlaneCount(0);
if (planeCount > (long) Integer.MAX_VALUE) {
parseErrorOccurred = true;
errorBuilder.append("Too many planes.\n");
}
final long planeWidth;
final long planeHeight;
final Plane plane = reader.openPlane(0, 0);
planeWidth = plane.getLengths()[0];
planeHeight = plane.getLengths()[1];
if ((planeWidth > (long) Integer.MAX_VALUE) ||
(planeHeight > (long) Integer.MAX_VALUE)) {
parseErrorOccurred = true;
errorBuilder.append(
"We are currently supporting planes with maximum size of Integer.MAX_VALUE x Integer" +
".MAX_VALUE");
} catch (final FormatException | IOException e) {
parseErrorOccurred = true;
errorBuilder.append("Unable to open first plane of the first image.\n")
.append(e.getMessage());
return;
}
setInputDataInfo(new FileInputData(inputFileArguments[0], new HyperStackDimensions((int) planeWidth,
(int) planeHeight,
(int) planeCount)));
getInputDataInfo().setDataLoaderType(InputData.DataLoaderType.SCIFIOLoader);
if (inputFileArguments.length > 1) {
parseInputFilePlaneOptions(errorBuilder, inputFileArguments, 1);
}
private void parseRawFileArguments(final StringBuilder errorBuilder, final String[] inputFileArguments) {
// We require the file path and dimensions, like input.raw 1920x1080x5
// First argument is input file name.
if (inputFileArguments.length < 2) {
parseErrorOccurred = true;
errorBuilder.append("Raw file requires its dimension as additional information.")
.append("e.g.: 1920x1080x1\n");
return;
}
getInputDataInfo().setDataLoaderType(InputData.DataLoaderType.RawDataLoader);
final Optional<HyperStackDimensions> parsedDatasetDims = ParseUtils.tryParseHyperStackDimensions(inputFileArguments[1], 'x');
if (parsedDatasetDims.isPresent()) {
setInputDataInfo(new FileInputData(inputFileArguments[0], parsedDatasetDims.get()));
parseErrorOccurred = true;
errorBuilder.append("Failed to parse image dimensions of format DxDxD. Got: ")
.append(inputFileArguments[1])
.append('\n');
return;
}
// User specified plane index or plane range.
if (inputFileArguments.length > 2) {
parseInputFilePlaneOptions(errorBuilder, inputFileArguments, 2);
}
}
/**
* Parse optional user specified plane index or plane range. (e.g. 5 or 5-50)
*
* @param errorBuilder String builder for the error message.
* @param inputFileArguments Input file arguments.
* @param inputFileArgumentsOffset Offset of the plane argument.
*/
private void parseInputFilePlaneOptions(final StringBuilder errorBuilder,
final String[] inputFileArguments,
final int inputFileArgumentsOffset) {
final int rangeSeparatorIndex = inputFileArguments[inputFileArgumentsOffset].indexOf("-");
final Optional<Range<Integer>> parsedRange =
ParseUtils.tryParseRange(inputFileArguments[inputFileArgumentsOffset], '-');
if (!parsedRange.isPresent()) {
parseErrorOccurred = true;
errorBuilder.append("Plane range index is wrong. Expected format D-D, got: ")
.append(inputFileArguments[inputFileArgumentsOffset]).append('\n');
} else {
getInputDataInfo().setPlaneRange(parsedRange.get());
// Here we parse single plane index option.
final Optional<Integer> parseResult = ParseUtils.tryParseInt(inputFileArguments[inputFileArgumentsOffset]);
if (parseResult.isPresent()) {
getInputDataInfo().setPlaneIndex(parseResult.get());
parseErrorOccurred = true;
errorBuilder.append("Failed to parse plane index option, expected integer, got: ")
.append(inputFileArguments[inputFileArgumentsOffset])
.append('\n');
/**
* Parse bits per codebook index.
*
* @param cmd Command line arguments.
* @param errorBuilder String error builder.
*/
private void parseBitsPerPixel(final CommandLine cmd, final StringBuilder errorBuilder) {
if (cmd.hasOption(CliConstants.BITS_LONG)) {
final String bitsString = cmd.getOptionValue(CliConstants.BITS_LONG);
final Optional<Integer> parseResult = ParseUtils.tryParseInt(bitsString);
if (parseResult.isPresent()) {
setBitsPerCodebookIndex(parseResult.get());
parseErrorOccurred = true;
errorBuilder.append("Failed to parse bits per pixel.").append('\n');
}
} else {
setBitsPerCodebookIndex(DEFAULT_BITS_PER_PIXEL);
/**
* Check if quantization type option is required for chosen program method..
*
* @param method Chosen program method.
* @return True if quantization type option is required.
*/
private boolean hasQuantizationType(final ProgramMethod method) {
return (method == ProgramMethod.Compress) ||
(method == ProgramMethod.Benchmark) ||
(method == ProgramMethod.TrainCodebook);
}
/**
* Parse compression type and vector dimensions for VQ.
*
* @param cmd Command line arguments.
* @param errorBuilder String error builder.
*/
private void parseCompressionType(final CommandLine cmd, final StringBuilder errorBuilder) {
if (cmd.hasOption(CliConstants.SCALAR_QUANTIZATION_LONG)) {
setQuantizationType(QuantizationType.Scalar);
} else if (cmd.hasOption(CliConstants.VECTOR_QUANTIZATION_LONG)) {
final String vectorDefinition = cmd.getOptionValue(CliConstants.VECTOR_QUANTIZATION_LONG);
final Optional<V3i> maybe3DVectorSize = ParseUtils.tryParseV3i(vectorDefinition, 'x');
if (maybe3DVectorSize.isPresent()) {
// Process 3D box dims.
setQuantizationType(QuantizationType.Vector1D);
if (maybe3DVectorSize.get().getY() > 1) {
setQuantizationType(QuantizationType.Vector2D);
if (maybe3DVectorSize.get().getZ() > 1) {
setQuantizationType(QuantizationType.Vector3D);
setQuantizationVector(maybe3DVectorSize.map(v3 -> new V3i(v3.getX(), v3.getY(), v3.getZ())).get());
return;
}
final Optional<V2i> maybe2DVectorSize = ParseUtils.tryParseV2i(vectorDefinition, 'x');
if (maybe2DVectorSize.isPresent()) {
// Process 2D matrix dims.
setQuantizationType(QuantizationType.Vector1D);
if (maybe2DVectorSize.get().getY() > 1) {
setQuantizationType(QuantizationType.Vector2D);
}
setQuantizationVector(maybe2DVectorSize.map(v2 -> new V3i(v2.getX(), v2.getY(), 1)).get());
return;
}
final Optional<Integer> maybe1DVectorSize = ParseUtils.tryParseInt(vectorDefinition);
if (maybe1DVectorSize.isPresent()) {
// Process 1D row dims.
setQuantizationType(QuantizationType.Vector1D);
setQuantizationVector(maybe1DVectorSize.map(xDim -> new V3i(xDim, 1, 1)).get());
} else {
parseErrorOccurred = true;
errorBuilder.append("Unable to determine vector dimensions from string: ")
.append(vectorDefinition)
.append(". Expected D, DxD or DxDxD.\n");
/**
* Parse chosen program method.
*
* @param cmd Command line arguments.
* @param errorBuilder String error builder.
*/
private void parseProgramMethod(final CommandLine cmd, final StringBuilder errorBuilder) {
if (cmd.hasOption(CliConstants.HELP_LONG)) {
method = ProgramMethod.PrintHelp;
} else if (cmd.hasOption(CliConstants.COMPRESS_LONG)) {
method = ProgramMethod.Compress;
} else if (cmd.hasOption(CliConstants.DECOMPRESS_LONG)) {
method = ProgramMethod.Decompress;
} else if (cmd.hasOption(CliConstants.BENCHMARK_LONG)) {
method = ProgramMethod.Benchmark;
} else if (cmd.hasOption(CliConstants.TRAIN_LONG)) {
method = ProgramMethod.TrainCodebook;
} else if (cmd.hasOption(CliConstants.INSPECT_LONG)) {
method = ProgramMethod.InspectFile;
} else if (cmd.hasOption(CliConstants.CUSTOM_FUNCTION_LONG)) {
method = ProgramMethod.CustomFunction;
parseErrorOccurred = true;
errorBuilder.append("No program method was matched\n");
}
}
public ProgramMethod getMethod() {
return method;
}
public boolean parseError() {
return parseErrorOccurred;
public String getParseError() {
return parseError;
final StringBuilder sb = new StringBuilder();
sb.append("Method: ");
switch (method) {
case Compress:
sb.append("Compress\n");
break;
case Decompress:
sb.append("Decompress\n");
break;
case Benchmark:
sb.append("Benchmark\n");
break;
case TrainCodebook:
sb.append("TrainCodebook\n");
break;
case PrintHelp:
sb.append("PrintHelp\n");
break;
case CustomFunction:
sb.append("CustomFunction\n");
break;
case InspectFile:
sb.append("InspectFile\n");
break;
}
if (hasQuantizationType(method)) {
switch (getQuantizationType()) {
case Scalar:
sb.append("Scalar\n");
break;
case Vector1D:
sb.append(String.format("Vector1D %s\n", getQuantizationVector().toString()));
sb.append(String.format("Vector2D %s\n", getQuantizationVector().toString()));
sb.append("InputFile: ").append(getInputDataInfo().getFilePath()).append('\n');
sb.append("Output: ").append(getOutputFilePath()).append('\n');
sb.append("BitsPerCodebookIndex: ").append(getBitsPerCodebookIndex()).append('\n');
switch (getCodebookType()) {
case MiddlePlane:
sb.append("Use middle plane for codebook training\n");
break;
case Global:
sb.append("CodebookCacheFolder: ").append(getCodebookCacheFolder()).append('\n');
break;
sb.append("Input image dims: ").append(getInputDataInfo().getDimensions().toString()).append('\n');
if (getInputDataInfo().isPlaneIndexSet()) {
sb.append("PlaneIndex: ").append(getInputDataInfo().getPlaneIndex()).append('\n');
if (getInputDataInfo().isPlaneRangeSet()) {
sb.append("FromPlaneIndex: ").append(getInputDataInfo().getPlaneRange().getFrom()).append('\n');
sb.append("ToPlaneIndex: ").append(getInputDataInfo().getPlaneRange().getTo()).append('\n');
sb.append("Verbose: ").append(isVerbose()).append('\n');
sb.append("ThreadWorkerCount: ").append(getWorkerCount()).append('\n');
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
@Override
public boolean isConsoleApplication() {
return true;
}