diff --git a/src/main/java/bdv/server/BigDataServer.java b/src/main/java/bdv/server/BigDataServer.java index 72a439cdf5e25b4f6766c090232c3d9df91f86d9..95350c8c9ff152ff9cac02db607a2b441121f937 100644 --- a/src/main/java/bdv/server/BigDataServer.java +++ b/src/main/java/bdv/server/BigDataServer.java @@ -57,8 +57,9 @@ import java.util.Optional; public class BigDataServer { private static final org.eclipse.jetty.util.log.Logger LOG = Log.getLogger(BigDataServer.class); - public static class ExtendedCompressionOptions extends CompressionOptions { + public final static class ExtendedCompressionOptions extends CompressionOptions { private int compressFromMipmapLevel; + private boolean enableCodebookTraining = false; public int getCompressFromMipmapLevel() { return compressFromMipmapLevel; @@ -67,8 +68,49 @@ public class BigDataServer { public void setCompressFromMipmapLevel(final int compressFromMipmapLevel) { this.compressFromMipmapLevel = compressFromMipmapLevel; } + + public boolean isCodebookTrainingEnabled() { + return enableCodebookTraining; + } + + public void setEnableCodebookTraining(final boolean enableCodebookTraining) { + this.enableCodebookTraining = enableCodebookTraining; + } + + public ExtendedCompressionOptions copyRequiredParams() { + final ExtendedCompressionOptions copy = new ExtendedCompressionOptions(); + copy.setQuantizationType(getQuantizationType()); + copy.setQuantizationVector(getQuantizationVector()); + copy.setWorkerCount(getWorkerCount()); + copy.setCodebookType(getCodebookType()); + copy.setCodebookCacheFolder(getCodebookCacheFolder()); + copy.setVerbose(isVerbose()); + copy.setCompressFromMipmapLevel(getCompressFromMipmapLevel()); + return copy; + } } + final static class BigDataServerDataset { + private final String xmlFile; + private final ExtendedCompressionOptions compressionOptions; + + BigDataServerDataset(final String xmlFile, + final ExtendedCompressionOptions compressionOptions) { + this.xmlFile = xmlFile; + this.compressionOptions = compressionOptions; + } + + public String getXmlFile() { + return xmlFile; + } + + public ExtendedCompressionOptions getCompressionOptions() { + return compressionOptions; + } + + } + + static Parameters getDefaultParameters() { final int port = 8080; @@ -83,7 +125,7 @@ public class BigDataServer { final boolean enableManagerContext = false; return new Parameters(port, hostname, - new HashMap<String, String>(), + new HashMap<String, BigDataServerDataset>(), thumbnailDirectory, baseUrl, enableManagerContext, @@ -117,8 +159,7 @@ public class BigDataServer { final ContextHandlerCollection datasetHandlers = createHandlers(baseURL, params.getDatasets(), - thumbnailsDirectoryName, - params.getCompressionParams()); + thumbnailsDirectoryName); handlers.addHandler(datasetHandlers); handlers.addHandler(new JsonDatasetListHandler(server, datasetHandlers)); @@ -154,7 +195,7 @@ public class BigDataServer { /** * maps from dataset name to dataset xml path. */ - private final Map<String, String> datasetNameToXml; + private final Map<String, BigDataServerDataset> datasetNameToXml; private final String thumbnailDirectory; @@ -166,7 +207,7 @@ public class BigDataServer { Parameters(final int port, final String hostname, - final Map<String, String> datasetNameToXml, + final Map<String, BigDataServerDataset> datasetNameToXml, final String thumbnailDirectory, final String baseUrl, final boolean enableManagerContext, @@ -201,7 +242,7 @@ public class BigDataServer { * * @return datasets as a map from dataset name to dataset xml path. */ - public Map<String, String> getDatasets() { + public Map<String, BigDataServerDataset> getDatasets() { return datasetNameToXml; } @@ -275,6 +316,9 @@ public class BigDataServer { options.addOption(new OptionWithOrder(CliConstants.createVerboseOption(false), ++optionOrder)); options.addOption(new OptionWithOrder(new Option(CompressFromShortKey, CompressFromLongKey, true, "Level from which the compression is enabled."), ++optionOrder)); + options.addOption(new OptionWithOrder(new Option(CliConstants.WORKER_COUNT_SHORT, CliConstants.WORKER_COUNT_LONG, + true, "Count of worker threads, which are used for codebook training."), + ++optionOrder)); if (Constants.ENABLE_EXPERIMENTAL_FEATURES) { @@ -300,47 +344,47 @@ public class BigDataServer { // Getting base url option final String baseUrl = cmd.getOptionValue("b", defaultParameters.getBaseUrl()); - final HashMap<String, String> datasets = new HashMap<String, String>(defaultParameters.getDatasets()); + final HashMap<String, BigDataServerDataset> datasets = + new HashMap<String, BigDataServerDataset>(defaultParameters.getDatasets()); final boolean enableQcmpCompression = cmd.hasOption(ENABLE_COMPRESSION); - final ExtendedCompressionOptions compressionOptions = new ExtendedCompressionOptions(); + final ExtendedCompressionOptions baseCompressionOptions = new ExtendedCompressionOptions(); if (enableQcmpCompression) { - compressionOptions.setQuantizationType(QuantizationType.Invalid); + baseCompressionOptions.setWorkerCount(Integer.parseInt(cmd.getOptionValue(CliConstants.WORKER_COUNT_LONG, "1"))); + baseCompressionOptions.setQuantizationType(QuantizationType.Invalid); if (cmd.hasOption(CliConstants.SCALAR_QUANTIZATION_LONG)) - compressionOptions.setQuantizationType(QuantizationType.Scalar); + baseCompressionOptions.setQuantizationType(QuantizationType.Scalar); else if (cmd.hasOption(CliConstants.VECTOR_QUANTIZATION_LONG)) { final String vqValue = cmd.getOptionValue(CliConstants.VECTOR_QUANTIZATION_LONG); final Optional<V2i> maybeV2 = ParseUtils.tryParseV2i(vqValue, 'x'); if (maybeV2.isPresent()) { - compressionOptions.setQuantizationType(QuantizationType.Vector2D); - compressionOptions.setQuantizationVector(new V3i(maybeV2.get().getX(), maybeV2.get().getY(), 1)); + baseCompressionOptions.setQuantizationType(QuantizationType.Vector2D); + baseCompressionOptions.setQuantizationVector(new V3i(maybeV2.get().getX(), maybeV2.get().getY(), 1)); } else { final Optional<V3i> maybeV3 = ParseUtils.tryParseV3i(vqValue, 'x'); if (maybeV3.isPresent()) { - compressionOptions.setQuantizationType(QuantizationType.Vector3D); - compressionOptions.setQuantizationVector(maybeV3.get()); + baseCompressionOptions.setQuantizationType(QuantizationType.Vector3D); + baseCompressionOptions.setQuantizationVector(maybeV3.get()); } } } - if (compressionOptions.getQuantizationType() == QuantizationType.Invalid) { + if (baseCompressionOptions.getQuantizationType() == QuantizationType.Invalid) { throw new ParseException("Invalid quantization type."); } - // NOTE(Moravec): Test if using more workers make any sense. Since the server is already handling multiple requests. - compressionOptions.setWorkerCount(1); - compressionOptions.setCodebookType(CompressionOptions.CodebookType.Global); - compressionOptions.setCodebookCacheFolder(cmd.getOptionValue(CliConstants.CODEBOOK_CACHE_FOLDER_LONG)); - compressionOptions.setVerbose(cmd.hasOption(CliConstants.VERBOSE_LONG)); + baseCompressionOptions.setCodebookType(CompressionOptions.CodebookType.Global); + baseCompressionOptions.setCodebookCacheFolder(cmd.getOptionValue(CliConstants.CODEBOOK_CACHE_FOLDER_LONG)); + baseCompressionOptions.setVerbose(cmd.hasOption(CliConstants.VERBOSE_LONG)); if (cmd.hasOption(CompressFromLongKey)) { - compressionOptions.setCompressFromMipmapLevel(Integer.parseInt(cmd.getOptionValue(CompressFromLongKey))); + baseCompressionOptions.setCompressFromMipmapLevel(Integer.parseInt(cmd.getOptionValue(CompressFromLongKey))); } final StringBuilder compressionReport = new StringBuilder(); compressionReport.append("\u001b[33m"); compressionReport.append("Quantization type: "); - switch (compressionOptions.getQuantizationType()) { + switch (baseCompressionOptions.getQuantizationType()) { case Scalar: compressionReport.append("Scalar"); break; @@ -354,11 +398,13 @@ public class BigDataServer { compressionReport.append("Vector3D"); break; } - compressionReport.append(compressionOptions.getQuantizationVector().toString()); + compressionReport.append(baseCompressionOptions.getQuantizationVector().toString()); compressionReport.append('\n'); - compressionReport.append("Codebook cache folder: ").append(compressionOptions.getCodebookCacheFolder()).append('\n'); - compressionReport.append("Verbose mode: ").append(compressionOptions.isVerbose() ? "ON" : "OFF").append('\n'); - compressionReport.append("CompressFromMipmapLevel: ").append(compressionOptions.getCompressFromMipmapLevel()).append('\n'); + compressionReport.append("Codebook cache folder: ").append(baseCompressionOptions.getCodebookCacheFolder()).append('\n'); + compressionReport.append("Verbose mode: ").append(baseCompressionOptions.isVerbose() ? "ON" : "OFF").append('\n'); + compressionReport.append("Worker count: ").append(baseCompressionOptions.getWorkerCount()).append('\n'); + compressionReport.append("CompressFromMipmapLevel: ").append(baseCompressionOptions.getCompressFromMipmapLevel()).append( + '\n'); compressionReport.append("\u001b[0m"); System.out.println(compressionReport.toString()); @@ -385,26 +431,39 @@ public class BigDataServer { for (final String str : lines) { final String[] tokens = str.split("\\s*\\t\\s*"); - if (tokens.length == 2 && StringUtils.isNotEmpty(tokens[0].trim()) && StringUtils.isNotEmpty(tokens[1].trim())) { + + if (tokens.length >= 2 && StringUtils.isNotEmpty(tokens[0].trim()) && StringUtils.isNotEmpty(tokens[1].trim())) { final String name = tokens[0].trim(); - final String xmlpath = tokens[1].trim(); - tryAddDataset(datasets, name, xmlpath); + final String xmlPath = tokens[1].trim(); + + final ExtendedCompressionOptions datasetCompressionOptions = baseCompressionOptions.copyRequiredParams(); + if (tokens.length == 3 && StringUtils.isNotEmpty(tokens[2].trim())) { + if (tokens[2].trim().equals("tcb")) { + datasetCompressionOptions.setEnableCodebookTraining(true); + } + } + + tryAddDataset(datasets, name, xmlPath, (enableQcmpCompression ? datasetCompressionOptions : null)); } else { LOG.warn("Invalid dataset file line (will be skipped): {" + str + "}"); } } } - // process additional {name, name.xml} pairs given on the - // command-line + // process additional {name, name.xml, [`tcb`]} pair or triplets given on the command-line final String[] leftoverArgs = cmd.getArgs(); - if (leftoverArgs.length % 2 != 0) - throw new IllegalArgumentException("Dataset list has an error while processing."); - for (int i = 0; i < leftoverArgs.length; i += 2) { - final String name = leftoverArgs[i]; - final String xmlpath = leftoverArgs[i + 1]; - tryAddDataset(datasets, name, xmlpath); + for (int i = 0; i < leftoverArgs.length; ) { + final String name = leftoverArgs[i++]; + final String xmlPath = leftoverArgs[i++]; + + final ExtendedCompressionOptions datasetCompressionOptions = baseCompressionOptions.copyRequiredParams(); + if ((i < leftoverArgs.length) && leftoverArgs[i].equals("tcb")) { + datasetCompressionOptions.setEnableCodebookTraining(true); + i++; + } + + tryAddDataset(datasets, name, xmlPath, (enableQcmpCompression ? datasetCompressionOptions : null)); } if (datasets.isEmpty()) @@ -416,7 +475,7 @@ public class BigDataServer { thumbnailDirectory, baseUrl, enableManagerContext, - enableQcmpCompression ? compressionOptions : null); + enableQcmpCompression ? baseCompressionOptions : null); } catch (final ParseException | IllegalArgumentException e) { LOG.warn(e.getMessage()); System.out.println(); @@ -439,18 +498,21 @@ public class BigDataServer { return null; } - private static void tryAddDataset(final HashMap<String, String> datasetNameToXML, + private static void tryAddDataset(final HashMap<String, BigDataServerDataset> datasetNameToXML, final String name, - final String xmlpath) throws IllegalArgumentException { + final String xmlPath, + final ExtendedCompressionOptions extendedCompressionOptions) throws IllegalArgumentException { for (final String reserved : Constants.RESERVED_CONTEXT_NAMES) if (name.equals(reserved)) throw new IllegalArgumentException("Cannot use dataset name: \"" + name + "\" (reserved for internal use)."); if (datasetNameToXML.containsKey(name)) throw new IllegalArgumentException("Duplicate dataset name: \"" + name + "\""); - if (Files.notExists(Paths.get(xmlpath))) - throw new IllegalArgumentException("Dataset file does not exist: \"" + xmlpath + "\""); - datasetNameToXML.put(name, xmlpath); - LOG.info("Dataset added: {" + name + ", " + xmlpath + "}"); + if (Files.notExists(Paths.get(xmlPath))) + throw new IllegalArgumentException("Dataset file does not exist: \"" + xmlPath + "\""); + + datasetNameToXML.put(name, new BigDataServerDataset(xmlPath, extendedCompressionOptions)); + LOG.info("Dataset added: {" + name + ", " + xmlPath + + ", QcmpDatasetTraining: " + ((extendedCompressionOptions.isCodebookTrainingEnabled()) ? "ON" : "OFF") + "}"); } private static String getThumbnailDirectoryPath(final Parameters params) throws IOException { @@ -480,19 +542,19 @@ public class BigDataServer { } private static ContextHandlerCollection createHandlers(final String baseURL, - final Map<String, String> dataSet, - final String thumbnailsDirectoryName, - final ExtendedCompressionOptions compressionOps) throws SpimDataException, - IOException { + final Map<String, BigDataServerDataset> dataSet, + final String thumbnailsDirectoryName) + throws SpimDataException, IOException { final ContextHandlerCollection handlers = new ContextHandlerCollection(); - for (final Entry<String, String> entry : dataSet.entrySet()) { + for (final Entry<String, BigDataServerDataset> entry : dataSet.entrySet()) { + final String name = entry.getKey(); - final String xmlpath = entry.getValue(); + final String xmlPath = entry.getValue().getXmlFile(); final String context = "/" + name; - final CellHandler ctx = new CellHandler(baseURL + context + "/", xmlpath, + final CellHandler ctx = new CellHandler(baseURL + context + "/", xmlPath, name, thumbnailsDirectoryName, - compressionOps); + entry.getValue().getCompressionOptions()); ctx.setContextPath(context); handlers.addHandler(ctx); } diff --git a/src/main/java/bdv/server/CellHandler.java b/src/main/java/bdv/server/CellHandler.java index 9bcc6ccfc3b715904af7a469e202115ea58eb55f..0a049b7e1a522ca6cdca01dd6580e4dc6c5abdba 100644 --- a/src/main/java/bdv/server/CellHandler.java +++ b/src/main/java/bdv/server/CellHandler.java @@ -17,18 +17,18 @@ import cz.it4i.qcmp.cache.QuantizationCacheManager; import cz.it4i.qcmp.compression.CompressionOptions; import cz.it4i.qcmp.compression.ImageCompressor; import cz.it4i.qcmp.data.V3i; -import cz.it4i.qcmp.io.FileInputData; -import cz.it4i.qcmp.io.FlatBufferInputData; -import cz.it4i.qcmp.io.InputData; -import cz.it4i.qcmp.io.MemoryOutputStream; +import cz.it4i.qcmp.io.*; import cz.it4i.qcmp.utilities.Stopwatch; import mpicbg.spim.data.SpimDataException; +import mpicbg.spim.data.generic.sequence.ImgLoaderHints; import net.imglib2.cache.CacheLoader; import net.imglib2.cache.LoaderCache; import net.imglib2.cache.ref.SoftRefLoaderCache; +import net.imglib2.img.array.ArrayImg; import net.imglib2.img.basictypeaccess.volatiles.array.VolatileShortArray; import net.imglib2.img.cell.Cell; import net.imglib2.realtransform.AffineTransform3D; +import net.imglib2.type.numeric.integer.UnsignedShortType; import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.handler.ContextHandler; import org.eclipse.jetty.util.log.Log; @@ -181,6 +181,7 @@ public class CellHandler extends ContextHandler { this.compressionParams = compressionOps; final Hdf5VolatileShortArrayLoader cacheArrayLoader = imgLoader.getShortArrayLoader(); + loader = key -> { final int[] cellDims = new int[]{ Integer.parseInt(key.parts[5]), @@ -207,7 +208,7 @@ public class CellHandler extends ContextHandler { thumbnailFilename = createThumbnail(spimData, baseFilename, datasetName, thumbnailsDirectory); final int numberOfMipmapLevels = imgLoader.getSetupImgLoader(0).numMipmapLevels(); - initializeCompression(numberOfMipmapLevels); + initializeCompression(numberOfMipmapLevels, imgLoader); } private ImageCompressor getCompressorForMipmapLevel(final int mipmapLevel) { @@ -218,7 +219,7 @@ public class CellHandler extends ContextHandler { return lowestResCompressor; } - private void initializeCompression(final int numberOfMipmapLevels) { + private void initializeCompression(final int numberOfMipmapLevels, final Hdf5ImageLoader hdf5ImageLoader) { if (compressionParams == null) return; this.compressionParams.setInputDataInfo(new FileInputData(this.baseFilename)); @@ -226,8 +227,24 @@ public class CellHandler extends ContextHandler { cachedCodebooks = qcm.loadAvailableCacheFiles(compressionParams); if (cachedCodebooks.isEmpty()) { - LOG.warn("Didn't find any cached codebook for " + this.baseFilename); - return; + if (compressionParams.isCodebookTrainingEnabled()) { + + // NOTE(Moravec): Train all possible codebooks from |L| 2 to 256. + if (!trainCompressionCodebooks(compressionParams, hdf5ImageLoader)) { + LOG.warn("Failed to train compression codebooks."); + return; + } + + cachedCodebooks = qcm.loadAvailableCacheFiles(compressionParams); + if (cachedCodebooks.isEmpty()) { + LOG.warn("Failed to train codebooks. Look above for errors."); + assert false; // For debug purposes. + return; + } + } else { + LOG.warn("Didn't find any cached codebooks for " + this.baseFilename); + return; + } } LOG.info(String.format("Found %d codebooks for %s.", cachedCodebooks.size(), this.baseFilename)); @@ -261,6 +278,38 @@ public class CellHandler extends ContextHandler { } } + private boolean trainCompressionCodebooks(final BigDataServer.ExtendedCompressionOptions compressionOptions, + final Hdf5ImageLoader hdf5ImageLoader) { + + final ArrayImg<?, ?> arrImg = (ArrayImg<?, ?>) hdf5ImageLoader.getSetupImgLoader(0).getImage(0, 0, ImgLoaderHints.LOAD_COMPLETELY); + assert (arrImg.numDimensions() == 3); + + assert (compressionOptions.getInputDataInfo().getCacheFileName().equals(baseFilename)); + + final CallbackInputData cid = new CallbackInputData((x, y, z) -> + { + return ((UnsignedShortType) arrImg.getAt(x, y, z)).getInteger(); + }, + compressionOptions.getInputDataInfo().getDimensions(), + baseFilename); + cid.setDimension(new V3i((int) arrImg.dimension(0), (int) arrImg.dimension(1), (int) arrImg.dimension(2))); + + + final InputData originalInputData = compressionOptions.getInputDataInfo(); + final boolean originalVerbose = compressionOptions.isVerbose(); + compressionOptions.setVerbose(true); + compressionOptions.setInputDataInfo(cid); + + + final ImageCompressor trainingCompressor = new ImageCompressor(compressionOptions); + final boolean result = trainingCompressor.trainAndSaveAllCodebooks(); + + compressionOptions.setInputDataInfo(originalInputData); + compressionOptions.setVerbose(originalVerbose); + + return result; + } + private synchronized MemoryOutputStream getCachedCompressionBuffer() { if (!cachedBuffers.empty()) {