/* * Copyright (C) 2015 Patryk Strach * * This file is part of Virtual Slide Viewer. * * Virtual Slide Viewer 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 3 of the License, or (at your option) any later version. * * Virtual Slide Viewer 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 Virtual Slide Viewer. * If not, see <http://www.gnu.org/licenses/>. */ package virtualslideviewer.bioformats; import java.awt.Dimension; import java.util.ArrayList; import java.util.List; import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.atomic.AtomicInteger; import virtualslideviewer.UncheckedInterruptedException; import virtualslideviewer.core.ImageIndex; import virtualslideviewer.core.Tile; import virtualslideviewer.core.VirtualSlideImage; import virtualslideviewer.util.ByteArrayPool; import virtualslideviewer.util.ParameterValidator; import virtualslideviewer.util.ThreadPoolUtil; /** * Class used to calculate a padding of virtual slide's image. * * The main purpose of this class was to calculate the amount of padding in a .vsi file but since v.5.1.0 bioformats started to read real * image size it is not needed anymore. It is kept nonetheless in case reading of some different format still behaves like reading * .vsi file before version 5.1.0 of bioformats library. */ public class PaddingCalculator { private final ByteArrayPool mTileDataBufferPool = new ByteArrayPool(); private final ExecutorService mThreadPool; public PaddingCalculator(ExecutorService threadPool) { ParameterValidator.throwIfNull(threadPool, "threadPool"); mThreadPool = threadPool; } /** * Computes the padding of an image at specified resolution index. * * The function computes padding in border tiles, only if image size is aligned to tile size, which is true in the case of .vsi files. * This function assumes that the padding has either pure white (255, 255, 255) or pure black (0, 0, 0) color. * Only the padding for first image index at specified resolution index is computed as the padding should * be the same regardless of channel, z plane and time point. * * @param image Image to compute padding for. * @param resIndex Resolution index at which the padding should be computed. * * @return The padding of image in pixels. */ public Dimension computePadding(VirtualSlideImage image, int resIndex) throws UncheckedInterruptedException { ParameterValidator.throwIfNull(image, "image"); int horizontalPadding = computeHorizontalPadding(image, resIndex); int verticalPadding = computeVerticalPadding(image, resIndex); if(horizontalPadding == image.getTileSize(resIndex).width || verticalPadding == image.getTileSize(resIndex).height) { // There is no padding, it's just white or black image. return new Dimension(0, 0); } return new Dimension(horizontalPadding, verticalPadding); } private int computeHorizontalPadding(VirtualSlideImage image, int resIndex) throws UncheckedInterruptedException { Dimension tileSize = image.getTileSize(resIndex); Dimension imageSize = image.getImageSize(resIndex); int tileColumns = imageSize.width / tileSize.width; int tileRows = imageSize.height / tileSize.height; if(imageSize.width % tileSize.width != 0) return 0; AtomicInteger maxPadding = new AtomicInteger(tileSize.width); List<Callable<Void>> tasks = new ArrayList<Callable<Void>>(); for(int row = 0; row < tileRows; row++) { Tile tile = new Tile(tileColumns - 1, row, new ImageIndex(resIndex, 0, 0, 0)); tasks.add(new TilePaddingComputationTask(image, tile, maxPadding, this::addTileToMaxHorizontalPaddingCalculation)); } ThreadPoolUtil.scheduleAndWait(mThreadPool, tasks); return maxPadding.get(); } private int addTileToMaxHorizontalPaddingCalculation(byte[] tileData, Dimension tileSize, int imageChannelCount, int maxPadding) { for(int y = 0; y < tileSize.height; y++) { for(int x = tileSize.width - 1; x >= tileSize.width - maxPadding; x--) { if(isPadding(tileData, x, y, tileSize.width, imageChannelCount)) { maxPadding = tileSize.width - (x + 1); break; } } if(maxPadding == 0) { return 0; } } return maxPadding; } private int computeVerticalPadding(VirtualSlideImage image, int resIndex) throws UncheckedInterruptedException { Dimension tileSize = image.getTileSize(resIndex); Dimension imageSize = image.getImageSize(resIndex); int tileColumns = imageSize.width / tileSize.width; int tileRows = imageSize.height / tileSize.height; if(imageSize.height % tileSize.height != 0) return 0; AtomicInteger maxPadding = new AtomicInteger(tileSize.height); List<Callable<Void>> tasks = new ArrayList<Callable<Void>>(); for(int column = 0; column < tileColumns; column++) { Tile tile = new Tile(column, tileRows - 1, new ImageIndex(resIndex, 0, 0, 0)); tasks.add(new TilePaddingComputationTask(image, tile, maxPadding, this::addTileToMaxVerticalPaddingCalculation)); } ThreadPoolUtil.scheduleAndWait(mThreadPool, tasks); return maxPadding.get(); } private int addTileToMaxVerticalPaddingCalculation(byte[] tileData, Dimension tileSize, int imageChannelCount, int maxPadding) { for(int x = 0; x < tileSize.width; x++) { for(int y = tileSize.height - 1; y >= tileSize.height - maxPadding; y--) { if(isPadding(tileData, x, y, tileSize.width, imageChannelCount)) { maxPadding = tileSize.height - (y + 1); break; } } if(maxPadding == 0) { return 0; } } return maxPadding; } private boolean isPadding(byte[] data, int x, int y, int width, int imageChannelCount) { // The 0s remove some entirely black tiles which one of the test samples of .vsi virtual slides had in lower right corner of image. // It occured only at 3 highest resolutions and had increasing sizes, respectively, 1x1, 2x1 and 3x1 tiles. int pixelIndex = (y * width + x) * imageChannelCount; for(int c = 0; c < imageChannelCount; c++) { if(data[pixelIndex + c] != (byte)255 && data[pixelIndex + c] != (byte)0) { return true; } } return false; } private interface PaddingCalculationStrategy { public int calculatePadding(byte[] imageData, Dimension imageSize, int imageChannelCount, int maxPadding); } private class TilePaddingComputationTask implements Callable<Void> { private final PaddingCalculationStrategy mCalculationStrategy; private final VirtualSlideImage mImage; private final AtomicInteger mMaxPadding; private final Tile mTile; private final Dimension mTileSize; private final int mImageChannelCount; public TilePaddingComputationTask(VirtualSlideImage image, Tile tile, AtomicInteger paddingReference, PaddingCalculationStrategy strategy) { ParameterValidator.throwIfNull(image, "image"); ParameterValidator.throwIfNull(paddingReference, "paddingReference"); ParameterValidator.throwIfNull(tile, "tile"); ParameterValidator.throwIfNull(strategy, "strategy"); mCalculationStrategy = strategy; mImage = image; mMaxPadding = paddingReference; mTile = tile; mTileSize = mTile.getBounds(mImage).getSize(); mImageChannelCount = (mImage.isRGB() ? 3 : 1); } @Override public Void call() { if(mMaxPadding.get() == 0) return null; byte[] tileData = mTileDataBufferPool.borrow(mTileSize.width * mTileSize.height * mImageChannelCount); { mImage.getTileData(tileData, mTile); int currentPadding = mCalculationStrategy.calculatePadding(tileData, mTileSize, mImageChannelCount, mMaxPadding.get()); mMaxPadding.accumulateAndGet(currentPadding, Math::min); } mTileDataBufferPool.putBack(tileData); return null; } } }