Skip to content
Snippets Groups Projects
CompressionOptionsCLIParser.java 20.5 KiB
Newer Older
  • Learn to ignore specific revisions
  • 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.cli.CommandLine;
    
    import org.apache.commons.io.FilenameUtils;
    
    import java.io.IOException;
    
    import java.nio.file.Paths;
    
    public class CompressionOptionsCLIParser extends CompressionOptions implements Cloneable {
    
        private static final int DEFAULT_BITS_PER_PIXEL = 8;
    
    
        /**
         * Chosen program method.
         */
    
        private ProgramMethod 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) {
    
            parseCLI(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) {
    
                    // Add compressed file extension.
    
                    defaultValue += CompressorDecompressorBase.EXTENSION;
    
                case Decompress: {
    
                    // 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;
    
            }
    
            return defaultValue;
    
        /**
         * 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) {
    
    
            if (inputFileArguments.length < 1) {
    
                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);
    
            } else {
    
                // 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("-");
    
            if (rangeSeparatorIndex != -1) {
    
    
                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());
    
                } else {
    
                    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());
    
                } else {
    
                    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 (hasQuantizationType(method)) {
    
                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;
    
            } else {
    
                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;
    
        public String report() {
    
            final StringBuilder sb = new StringBuilder();
    
    
            sb.append("Method: ");
            switch (method) {
                case Compress:
                    sb.append("Compress\n");
                    break;
                case Decompress:
                    sb.append("Decompress\n");
                    break;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                case Benchmark:
                    sb.append("Benchmark\n");
                    break;
                case TrainCodebook:
                    sb.append("TrainCodebook\n");
                    break;
    
                case PrintHelp:
                    sb.append("PrintHelp\n");
                    break;
    
    Vojtech Moravec's avatar
    Vojtech Moravec committed
                case CustomFunction:
                    sb.append("CustomFunction\n");
                    break;
    
                case InspectFile:
                    sb.append("InspectFile\n");
                    break;
            }
    
    
    
            if (hasQuantizationType(method)) {
    
                sb.append("Quantization type: ");
    
                switch (getQuantizationType()) {
    
                    case Scalar:
                        sb.append("Scalar\n");
                        break;
                    case Vector1D:
    
                        sb.append(String.format("Vector1D %s\n", getQuantizationVector().toString()));
    
                        break;
                    case Vector2D:
    
                        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;
    
            if (hasQuantizationType(method)) {
    
                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');
    
            return sb.toString();
        }
    
        @Override
        public Object clone() throws CloneNotSupportedException {
            return super.clone();
        }
    
    
        @Override
        public boolean isConsoleApplication() {
            return true;
        }