All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.algart.maps.pyramids.io.api.AbstractPlanePyramidSource Maven / Gradle / Ivy

Go to download

Open-source libraries, providing the base functions for SciChains product in area of computer vision.

There is a newer version: 4.4.9
Show newest version
/*
 * The MIT License (MIT)
 *
 * Copyright (c) 2017-2024 Daniel Alievsky, AlgART Laboratory (http://algart.net)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package net.algart.maps.pyramids.io.api;

import net.algart.arrays.Arrays;
import net.algart.arrays.*;
import net.algart.maps.pyramids.io.api.sources.RotatingPlanePyramidSource;
import net.algart.math.IPoint;
import net.algart.math.IRectangularArea;
import net.algart.math.Range;
import net.algart.math.functions.LinearFunc;

import java.awt.*;
import java.nio.channels.NotYetConnectedException;
import java.util.List;
import java.util.*;
import java.util.concurrent.atomic.AtomicReference;

public abstract class AbstractPlanePyramidSource implements PlanePyramidSource {
    private static final int MAX_NON_TILED_READING_DIM = Math.max(16, Arrays.SystemSettings.getIntProperty(
            "net.algart.maps.pyramids.io.maxNonTiledReadingDim", 4096));
    // 64 MB for packed int ARGB
    private static final int READING_TILE_DIM = (int) Math.min(16384, Arrays.SystemSettings.getLongProperty(
            "net.algart.maps.pyramids.io.readingTile", 2 * DEFAULT_TILE_DIM));
    private static final long TILE_CACHING_MEMORY = Math.max(16, Arrays.SystemSettings.getLongProperty(
            "net.algart.maps.pyramids.io.tileCachingMemory", 67108864));
    // 64 MB (+1 possible additional tile)

    public enum TileDirection {
        RIGHT_DOWN() {
            @Override
            IRectangularArea findTile(long tileDim, long dimX, long dimY, long x, long y) {
                assert dimX > 0 && dimY > 0;
                assert tileDim > 0;
                assert x >= 0 && y >= 0 && x < dimX && y < dimY;
                long minX = x - x % tileDim;
                long minY = y - y % tileDim;
                long maxX = Math.min(minX + tileDim, dimX) - 1;
                long maxY = Math.min(minY + tileDim, dimY) - 1;
                return IRectangularArea.valueOf(
                        IPoint.valueOf(minX, minY),
                        IPoint.valueOf(maxX, maxY));
            }
        },
        LEFT_DOWN() {
            @Override
            IRectangularArea findTile(long tileDim, long dimX, long dimY, long x, long y) {
                assert dimX > 0 && dimY > 0;
                assert tileDim > 0;
                assert x >= 0 && y >= 0 && x < dimX && y < dimY;
                x = dimX - 1 - x;
                long minX = x - x % tileDim;
                long minY = y - y % tileDim;
                long maxX = Math.min(minX + tileDim, dimX) - 1;
                long maxY = Math.min(minY + tileDim, dimY) - 1;
                return IRectangularArea.valueOf(
                        IPoint.valueOf(dimX - 1 - maxX, minY),
                        IPoint.valueOf(dimX - 1 - minX, maxY));
            }
        },
        RIGHT_UP() {
            @Override
            IRectangularArea findTile(long tileDim, long dimX, long dimY, long x, long y) {
                assert dimX > 0 && dimY > 0;
                assert tileDim > 0;
                assert x >= 0 && y >= 0 && x < dimX && y < dimY;
                y = dimY - 1 - y;
                long minX = x - x % tileDim;
                long minY = y - y % tileDim;
                long maxX = Math.min(minX + tileDim, dimX) - 1;
                long maxY = Math.min(minY + tileDim, dimY) - 1;
                return IRectangularArea.valueOf(
                        IPoint.valueOf(minX, dimY - 1 - maxY),
                        IPoint.valueOf(maxX, dimY - 1 - minY));
            }
        },
        LEFT_UP() {
            @Override
            IRectangularArea findTile(long tileDim, long dimX, long dimY, long x, long y) {
                assert dimX > 0 && dimY > 0;
                assert tileDim > 0;
                assert x >= 0 && y >= 0 && x < dimX && y < dimY;
                x = dimX - 1 - x;
                y = dimY - 1 - y;
                long minX = x - x % tileDim;
                long minY = y - y % tileDim;
                long maxX = Math.min(minX + tileDim, dimX) - 1;
                long maxY = Math.min(minY + tileDim, dimY) - 1;
                return IRectangularArea.valueOf(
                        IPoint.valueOf(dimX - 1 - maxX, dimY - 1 - maxY),
                        IPoint.valueOf(dimX - 1 - minX, dimY - 1 - minY));
            }
        };

        abstract IRectangularArea findTile(long tileDim, long dimX, long dimY, long x, long y);
    }

    public enum LabelPosition {
        LEFT_OF_THE_MAP,
        RIGHT_OF_THE_MAP
    }

    private static final System.Logger LOG = System.getLogger(AbstractPlanePyramidSource.class.getName());

    private MemoryModel memoryModel = Arrays.SMM;
    private boolean skipCoarseData = false;
    private double skippingFiller = 0.0;

    private TileDirection tileCacheDirection = null;
    private volatile long tileCachingMemory = TILE_CACHING_MEMORY;

    private volatile RotatingPlanePyramidSource.RotationMode labelRotation =
            RotatingPlanePyramidSource.RotationMode.NONE;
    private volatile Color labelRotationBackground = Color.GRAY;

    private final AtomicReference tileCacheContainer = new AtomicReference();
    // - here must be a reference, not a field: this object is usually cloned, and corrections in a clone
    // do not affect the original; but this reference is shared with all clones

    private final SpeedInfo speedInfo = new SpeedInfo();

    protected AbstractPlanePyramidSource() {
    }

    public final MemoryModel getMemoryModel() {
        return memoryModel;
    }

    public final void setMemoryModel(MemoryModel memoryModel) {
        this.memoryModel = Objects.requireNonNull(memoryModel, "Null memoryModel");
    }

    /**
     * Enforces some plane pyramid sources to skip (usually to stay zero) coarse data, which should not
     * be processed by image processing algorithms, like {@link SpecialImageKind#WHOLE_SLIDE}.
     *
     * @return the flag; false by default.
     */
    public final boolean isSkipCoarseData() {
        return skipCoarseData;
    }

    public final void setSkipCoarseData(boolean skipCoarseData) {
        this.skipCoarseData = skipCoarseData;
    }

    public final double getSkippingFiller() {
        return skippingFiller;
    }

    public final void setSkippingFiller(double skippingFiller) {
        this.skippingFiller = skippingFiller;
    }


    public abstract int numberOfResolutions();

    public int compression() {
        return PlanePyramidTools.defaultCompression(this);
    }

    public abstract int bandCount();

    public boolean isResolutionLevelAvailable(int resolutionLevel) {
        return true;
    }

    public boolean[] getResolutionLevelsAvailability() {
        boolean[] result = new boolean[numberOfResolutions()];
        for (int k = 0; k < result.length; k++) {
            result[k] = isResolutionLevelAvailable(k);
        }
        return result;
    }

    public abstract long[] dimensions(int resolutionLevel) throws NoSuchElementException;

    public abstract long dim(int resolutionLevel, int index);

    public boolean isElementTypeSupported() {
        return false;
    }

    public Class elementType() throws UnsupportedOperationException {
        throw new UnsupportedOperationException("elementType() method is not supported by "
                + super.getClass()); // avoiding IDEA bug
    }

    @Override
    public List>> zeroLevelActualAreaBoundaries() {
        final List rectangles = zeroLevelActualRectangles();
        if (rectangles == null) {
            return null;
        }
        final List>> result = new ArrayList>>();
        for (IRectangularArea rectangle : rectangles) {
            final List vertices = new ArrayList();
            vertices.add(rectangle.min());
            vertices.add(IPoint.valueOf(rectangle.max(0), rectangle.min(1)));
            vertices.add(rectangle.max());
            vertices.add(IPoint.valueOf(rectangle.min(0), rectangle.max(1)));
            result.add(Collections.singletonList(vertices));
        }
        return result;
    }

    // Note: this implementation cannot read data outside the full matrix
    public Matrix readSubMatrix(
            int resolutionLevel, long fromX, long fromY, long toX, long toY)
            throws NoSuchElementException, NotYetConnectedException {
        final int bandCount = bandCount();
        final long[] dimensions = dimensions(resolutionLevel);
        checkSubMatrixRanges(dimensions, fromX, fromY, toX, toY, false);
        final long totalElements = Arrays.longMul(bandCount, toX - fromX, toY - fromY);
        assert totalElements != Long.MIN_VALUE; // because of the check above
        if (fromX == toX || fromY == toY
                || (!isTileCachingEnabled() && Math.max(toX - fromX, toY - fromY) <= MAX_NON_TILED_READING_DIM)) {
            return readSubMatrixViaTileCache(resolutionLevel, fromX, fromY, toX, toY, null);
        }
        Matrix result = null;
        long readyElements = 0;
        TileDirection direction = isTileCachingEnabled() ? getTileCacheDirection() : TileDirection.RIGHT_DOWN;
        final long dimX = dimensions[1];
        final long dimY = dimensions[2];
        final int readingTileDim = readingTileDim();
        IRectangularArea leftTile; // - the left tile in each row
        for (long y = fromY; y < toY; y = leftTile.max(1) + 1) {
            leftTile = direction.findTile(readingTileDim, dimX, dimY, fromX, y);
            IRectangularArea tile;
            for (long x = fromX; x < toX; x = tile.max(0) + 1) {
                tile = direction.findTile(readingTileDim, dimX, dimY, x, y);
                final long tileFromX = Math.max(tile.min(0), fromX);
                final long tileFromY = Math.max(tile.min(1), fromY);
                final long tileToX = Math.min(tile.max(0) + 1, toX);
                final long tileToY = Math.min(tile.max(1) + 1, toY);
                assert tileFromX <= tileToX;
                assert tileFromY <= tileToY;
                final Matrix m = readSubMatrixViaTileCache(
                        resolutionLevel,
                        tileFromX, tileFromY, tileToX, tileToY,
                        tile);
                if (fromX == tileFromX && fromY == tileFromY && toX == tileToX && toY == tileToY) {
                    // it is the only tile which should be loaded: we already have the final result
                    assert result == null : "Unexpected non-null result = " + result + " for tile "
                            + tileFromX + ".." + tileToX + "x" + tileFromY + ".." + tileToY;
                    LOG.log(System.Logger.Level.TRACE, () -> AbstractPlanePyramidSource.class.getSimpleName()
                            + " quickly returned result: " + m);
                    return m;
                }
                if (result == null) {
                    final MemoryModel mm = Arrays.sizeOf(m.elementType(), totalElements) <=
                            Arrays.SystemSettings.maxTempJavaMemory() ?
                            Arrays.SMM :
                            memoryModel;
                    result = mm.newMatrix(UpdatablePArray.class,
                            m.elementType(), bandCount, toX - fromX, toY - fromY);
                    if (!SimpleMemoryModel.isSimpleArray(result.array())) {
                        result = result.tile(bandCount, DEFAULT_TILE_DIM, DEFAULT_TILE_DIM);
                    }
                    LOG.log(System.Logger.Level.TRACE, AbstractPlanePyramidSource.class.getSimpleName()
                            + " created result " + result);
                }
                final Matrix subMatrix = result.subMatrix(
                        0, tileFromX - fromX, tileFromY - fromY,
                        bandCount, tileToX - fromX, tileToY - fromY);
                if (!m.dimEquals(subMatrix)) {
                    throw new AssertionError("Internal bug in readSubMatrixViaCache: "
                            + "incorrect dimensions of the result "
                            + m.dim(0) + "x" + m.dim(1) + "x" + m.dim(2)
                            + " instead of " + bandCount + "x" + (tileToX - tileFromX) + "x" + (tileToY - tileFromY));
                }
                subMatrix.array().copy(m.array());
                readyElements += m.size();
            }
        }
        return result;
    }

    public Matrix readFullMatrix(int resolutionLevel)
            throws NoSuchElementException, NotYetConnectedException, UnsupportedOperationException {
        if (!isFullMatrixSupported()) {
            throw new UnsupportedOperationException("readFullMatrix method is not supported");
        }
        final long[] dimensions = dimensions(resolutionLevel);
        return readSubMatrix(resolutionLevel, 0, 0, dimensions[1], dimensions[2]);
    }

    public boolean isSpecialMatrixSupported(SpecialImageKind kind) {
        return false;
    }

    public Optional> readSpecialMatrix(SpecialImageKind kind)
            throws NotYetConnectedException {
        Objects.requireNonNull(kind, "Null image kind");
        if (kind == SpecialImageKind.SMALLEST_LAYER) {
            int resolutionLevel = numberOfResolutions() - 1;
            final long[] dimensions = dimensions(resolutionLevel);
            return Optional.of(readSubMatrix(resolutionLevel, 0, 0, dimensions[1], dimensions[2]));
        } else {
            return Optional.empty();
        }
    }

    public boolean isDataReady() {
        return true;
    }

    /**
     * This implementation does nothing.
     *
     * 

If your implementation overrides this method, it must call super.loadResources at the end — * because it is possible that future version will do something useful. */ public void loadResources() { } /** * This implementation frees the tile cache. * * @param flushMode possible strategy of freeing resources (ignored by this implementation). */ public void freeResources(FlushMode flushMode) { synchronized (tileCacheContainer) { if (tileCacheContainer.get() != null) { LOG.log(System.Logger.Level.DEBUG, () -> AbstractPlanePyramidSource.class.getSimpleName() + " is freeing tile cache"); tileCacheContainer.set(null); } } } public void fillBySkippingFiller(Matrix matrix, boolean fillWhenZero) { if (skipCoarseData) { if (fillWhenZero || skippingFiller != 0.0) { final double filler = skippingFiller * matrix.array().maxPossibleValue(1.0); matrix.array().fill(filler); } } } public Matrix constantMatrixSkippingFiller(Class elementType, long dimX, long dimY) { final Class arrayType = Arrays.type(PArray.class, elementType); final double filler = skippingFiller * Arrays.maxPossibleValue(arrayType, 1.0); return Matrices.constantMatrix(filler, arrayType, bandCount(), dimX, dimY); } public final boolean isTileCachingEnabled() { return tileCacheDirection != null; } public final TileDirection getTileCacheDirection() { if (this.tileCacheDirection == null) { throw new IllegalStateException("Tile caching is disabled: cannot read tile cache direction"); } return this.tileCacheDirection; } public final void enableTileCaching(TileDirection tileDirection) { if (tileDirection == null) { throw new NullPointerException("Null tileCacheDirection argument"); } this.tileCacheDirection = tileDirection; } public final void disableTileCaching() { this.tileCacheDirection = null; } /** * Returns the size in bytes of the cache, used when {@link #isTileCachingEnabled()}. * The real amount memory, used by this instance of this class, can me little greater (approximately * by the size of 1 tile). * *

The initial value is retrieved from the system property * "net.algart.maps.pyramids.io.maxNonTiledReadingDim", * if it exists and contains a valid integer number. * If there is no such property, or if it contains not a number, * or if some exception occurred while calling Long.getLong, * the default value 67108864 (64 MB) is used. * * @return the amount of memory in bytes, which is allowed to use for caching by this instance. */ public final long getTileCachingMemory() { return tileCachingMemory; } public final void setTileCachingMemory(long tileCachingMemory) { if (tileCachingMemory < 0) { throw new IllegalArgumentException("Negative tileCachingMemory"); } this.tileCachingMemory = tileCachingMemory; } public final RotatingPlanePyramidSource.RotationMode getLabelRotation() { return labelRotation; } public final void setLabelRotation(RotatingPlanePyramidSource.RotationMode labelRotation) { if (labelRotation == null) { throw new NullPointerException("Null labelRotation"); } this.labelRotation = labelRotation; } public final Color getLabelRotationBackground() { return labelRotationBackground; } public final void setLabelRotationBackground(Color labelRotationBackground) { if (labelRotationBackground == null) { throw new NullPointerException("Null labelRotationBackground"); } this.labelRotationBackground = labelRotationBackground; } public final void checkSubMatrixRanges( int resolutionLevel, long fromX, long fromY, long toX, long toY, boolean require31BitSize) { checkSubMatrixRanges(dimensions(resolutionLevel), fromX, fromY, toX, toY, require31BitSize); } /** * Returns the size (width and height) of a tile, used for cache (when {@link #isTileCachingEnabled()} * and for splitting large submatrix for reading into smaller tiles, read by {@link #readLittleSubMatrix}. * *

The default implementation retrieves the value from the system property * "net.algart.maps.pyramids.io.tileCachingMemory", * if it exists and contains a valid integer number. * If there is no such property, or if it contains not a number, * or if some exception occurred while calling Long.getLong, * this method returns the default value 4096. * *

The result must be positive. * *

The result must be stable (constant) for the given instance. * *

This method must work quickly. * * @return the width/height of tiles for caching and splitting large read submatrix. */ protected int readingTileDim() { return READING_TILE_DIM; } protected abstract Matrix readLittleSubMatrix( int resolutionLevel, long fromX, long fromY, long toX, long toY) throws NoSuchElementException, NotYetConnectedException; protected final Matrix makeWholeSlideFromLabelAndMap(LabelPosition labelPosition) { if (labelPosition == null) { throw new NullPointerException("Null labelPosition"); } long t1 = System.nanoTime(); final MapOrLabelParallelReader labelReader = new MapOrLabelParallelReader(SpecialImageKind.LABEL_ONLY_IMAGE); final MapOrLabelParallelReader mapReader = new MapOrLabelParallelReader(SpecialImageKind.MAP_IMAGE); final Thread labelReaderThread = new Thread(labelReader); final Thread mapReaderThread = new Thread(mapReader); labelReaderThread.start(); mapReaderThread.start(); try { labelReaderThread.join(); mapReaderThread.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); } final long labelDimX = labelReader.data.dim(DIM_WIDTH); final long labelDimY = labelReader.data.dim(DIM_HEIGHT); final long mapDimX = mapReader.data.dim(DIM_WIDTH); final long mapDimY = mapReader.data.dim(DIM_HEIGHT); long t2 = System.nanoTime(); final long wholeSlideDimX = wholeSlideDimX(mapDimX, mapDimY, labelDimX, labelDimY); final long wholeSlideDimY = mapDimY; final int bandCount = bandCount(); final Matrix result = Arrays.SMM.newByteMatrix( bandCount, wholeSlideDimX, wholeSlideDimY); // There is no sense to use better precision for a special matrix; // at the same time, better precision can lead to problems while saving the generated // whole slide image into some formats, for example, into BufferedImage or JPEG file final long resultDimX = result.dim(DIM_WIDTH); long t3 = System.nanoTime(); Matrices.copy(null, result.subMatr( 0, labelPosition == LabelPosition.RIGHT_OF_THE_MAP ? 0 : resultDimX - mapDimX, 0, bandCount, mapDimX, mapDimY), mapReader.data ); long t4 = System.nanoTime(); Matrices.resize(null, Matrices.ResizingMethod.POLYLINEAR_AVERAGING, result.subMatr( 0, labelPosition == LabelPosition.RIGHT_OF_THE_MAP ? mapDimX : 0, 0, bandCount, resultDimX - mapDimX, mapDimY), labelReader.data ); long t5 = System.nanoTime(); LOG.log(System.Logger.Level.DEBUG, () -> String.format(Locale.US, "Combining MAP and LABEL: %.3f ms " + "(%.3f parallel reading " + "[%.3f reading LABEL + %.3f rotating LABEL + %.3f + %.3f correcting LABEL || %.3f reading MAP] + " + "%.3f allocating result + %.3f inserting MAP + %.3f inserting resized LABEL)", (t4 - t1) * 1e-6, (t2 - t1) * 1e-6, labelReader.readingTime * 1e-6, labelReader.rotatingTime * 1e-6, labelReader.correctingBitDepthTime * 1e-6, labelReader.correctingBandCountTime * 1e-6, mapReader.readingTime * 1e-6, (t3 - t2) * 1e-6, (t4 - t3) * 1e-6, (t5 - t4) * 1e-6)); return result; } protected final Matrix newResultMatrix(long dimX, long dimY) { Matrix result = memoryModel.newMatrix( Arrays.SystemSettings.maxTempJavaMemory(), UpdatablePArray.class, elementType(), bandCount(), dimX, dimY); if (!SimpleMemoryModel.isSimpleArray(result.array())) { result = result.tile(result.dim(0), 1024, 1024); } return result; } protected final Matrix newFilledResultMatrix(long dimX, long dimY, Color backgroundColor) { final Matrix result = newResultMatrix(dimX, dimY); if (isSkipCoarseData()) { fillBySkippingFiller(result, false); } else { PlanePyramidTools.fillMatrix(result, backgroundColor); } return result; } public static List defaultZeroLevelActualRectangles(PlanePyramidSource source) { if (source == null) { throw new NullPointerException("Null source argument"); } final long[] dimensions = source.dimensions(0); if (dimensions[DIM_WIDTH] == 0 || dimensions[DIM_HEIGHT] == 0) { return null; } else { return Collections.singletonList(IRectangularArea.valueOf( IPoint.valueOf(0, 0), IPoint.valueOf(dimensions[DIM_WIDTH] - 1, dimensions[DIM_HEIGHT - 1]))); } } public static void checkSubMatrixRanges( long[] dimensions, long fromX, long fromY, long toX, long toY, boolean require31BitSize) { if (Arrays.longMul(dimensions) == Long.MIN_VALUE) { throw new TooLargeArrayException("Product of all dimensions >Long.MAX_VALUE"); } final long dimX = dimensions[1]; final long dimY = dimensions[2]; final long bandCount = dimensions[0]; if (fromX < 0 || fromY < 0 || fromX > toX || fromY > toY || toX > dimX || toY > dimY) { throw new IndexOutOfBoundsException("Illegal fromX..toX=" + fromX + ".." + toX + " or fromY..toY=" + fromY + ".." + toY + ": must be in ranges 0.." + dimX + ", 0.." + dimY + ", fromX<=toX, fromY<=toY"); } // Be careful: fromX == toX == dimX are NOT incorrect arguments, as any situation fromx < toX == dimX if (require31BitSize) { if (toX - fromX > Integer.MAX_VALUE || toY - fromY > Integer.MAX_VALUE || (toX - fromX) * (toY - fromY) >= Integer.MAX_VALUE / bandCount) { throw new IllegalArgumentException("Too large rectangle " + (toX - fromX) + "x" + (toY - fromY)); } } } public static long wholeSlideDimX(long mapDimX, long mapDimY, long labelDimX, long labelDimY) { return mapDimX + Math.round(labelDimX * (double) mapDimY / (double) labelDimY); } private Matrix readSubMatrixViaTileCache( int resolutionLevel, long fromX, long fromY, long toX, long toY, IRectangularArea containingTile) throws NoSuchElementException, NotYetConnectedException { if (fromX == toX || fromY == toY || !isTileCachingEnabled()) { return callAndCheckReadLittleSubMatrix(resolutionLevel, fromX, fromY, toX, toY); } if (containingTile == null) { throw new AssertionError("Internal bug in readSubMatrix implementation: " + "tile cache is enabled, by the containing tile is null (not calculated?)"); } if (!(containingTile.min(0) <= fromX && toX <= containingTile.max(0) + 1 && containingTile.min(1) <= fromY && toY <= containingTile.max(1) + 1 && fromX < toX && fromY < toY)) { throw new AssertionError("Internal bug in readSubMatrix implementation: " + "the containing tile " + containingTile + " does not contain the required area " + fromX + ".." + (toX - 1) + " x " + fromY + ".." + (toY - 1) + " or this area is negative"); } Matrix tileData; synchronized (tileCacheContainer) { if (tileCacheContainer.get() == null) { tileCacheContainer.set(new TileCache(readingTileDim(), tileCachingMemory)); } tileData = tileCacheContainer.get().getTile(resolutionLevel, containingTile); if (tileData == null) { tileData = callAndCheckReadLittleSubMatrix( resolutionLevel, containingTile.min(0), containingTile.min(1), containingTile.max(0) + 1, containingTile.max(1) + 1); if (!(SimpleMemoryModel.isSimpleArray(tileData.array()) || Arrays.isNCopies(tileData.array()))) { tileData = tileData.matrix(tileData.array().updatableClone(Arrays.SMM)); } tileCacheContainer.get().putTile(resolutionLevel, containingTile, tileData); } } return tileData.subMatrix( 0, fromX - containingTile.min(0), fromY - containingTile.min(1), tileData.dim(0), toX - containingTile.min(0), toY - containingTile.min(1)); } private Matrix callAndCheckReadLittleSubMatrix( int resolutionLevel, long fromX, long fromY, long toX, long toY) throws NoSuchElementException, NotYetConnectedException { long t1 = System.nanoTime(); final Matrix m = readLittleSubMatrix(resolutionLevel, fromX, fromY, toX, toY); long t2 = System.nanoTime(); if (m.dim(0) != bandCount() || m.dim(1) != toX - fromX || m.dim(2) != toY - fromY) { throw new AssertionError("Illegal implementation of readLittleSubMatrix: " + "incorrect dimensions of the result " + m.dim(0) + "x" + m.dim(1) + "x" + m.dim(2) + " instead of " + bandCount() + "x" + (toX - fromX) + "x" + (toY - fromY)); } final String averageSpeed = speedInfo.update(Matrices.sizeOf(m), t2 - t1); LOG.log(System.Logger.Level.DEBUG, () -> String.format(Locale.US, "%s has read (level %d): " + "%d..%d x %d..%d (%d x %d) in %.5f ms, %.3f MB/sec, average %s (reader: %s)", AbstractPlanePyramidSource.class.getSimpleName(), resolutionLevel, fromX, toX, fromY, toY, toX - fromX, toY - fromY, (t2 - t1) * 1e-6, Matrices.sizeOf(m) / 1048576.0 / ((t2 - t1) * 1e-9), averageSpeed, super.getClass().getSimpleName() )); return m; } private Matrix rotateLabelImage(final Matrix label) { if (labelRotation == RotatingPlanePyramidSource.RotationMode.NONE) { return label; } final Matrix rotated = labelRotation.asRotated(label); long newWidth = rotated.dim(DIM_WIDTH); long newHeight = rotated.dim(DIM_HEIGHT); if (newWidth > label.dim(DIM_WIDTH)) { newHeight *= (double) label.dim(DIM_WIDTH) / newWidth; newWidth = label.dim(DIM_WIDTH); } if (newHeight > label.dim(DIM_HEIGHT)) { newWidth *= (double) label.dim(DIM_HEIGHT) / newHeight; newHeight = label.dim(DIM_HEIGHT); } assert newWidth <= label.dim(DIM_WIDTH); assert newHeight <= label.dim(DIM_HEIGHT); final Matrix result = Arrays.SMM.newMatrix(UpdatablePArray.class, label); PlanePyramidTools.fillMatrix(result, labelRotationBackground); final Matrix centralArea = result.subMatr( 0, (result.dim(DIM_WIDTH) - newWidth) / 2, (result.dim(DIM_HEIGHT) - newHeight) / 2, result.dim(0), newWidth, newHeight); Matrices.resize(null, Matrices.ResizingMethod.POLYLINEAR_AVERAGING, centralArea, rotated); return result; } protected final class WholeSlideScaler { private final int suitableWholeSlideLevel; private final Matrix suitableWholeSlide; private final double scaleX; private final double scaleY; Matrix resultBackground = null; volatile long fromXAtSlide; volatile long fromYAtSlide; volatile long toXAtSlide; volatile long toYAtSlide; long scalingTime = 0; public WholeSlideScaler(List> wholeSlidePyramid, int resolutionLevel) { if (wholeSlidePyramid == null) { throw new NullPointerException("Null wholeSlidePyramid"); } final long[] dim = dimensions(resolutionLevel); final long levelDimX = dim[DIM_WIDTH]; final long levelDimY = dim[DIM_HEIGHT]; int suitableLevel = 0; for (int k = 1; k < wholeSlidePyramid.size(); k++) { Matrix m = wholeSlidePyramid.get(k); if (m.dim(DIM_WIDTH) >= levelDimX && m.dim(DIM_HEIGHT) >= levelDimY) { suitableLevel = k; } else { break; } } this.suitableWholeSlideLevel = suitableLevel; Matrix matrix = wholeSlidePyramid.get(suitableLevel); final Class requiredElementType = elementType(); if (requiredElementType != matrix.elementType()) { Class destType = Arrays.type(PArray.class, requiredElementType); final Range srcRange = Range.valueOf(0.0, matrix.array().maxPossibleValue(1.0)); final Range destRange = Range.valueOf(0.0, Arrays.maxPossibleValue(destType, 1.0)); matrix = Matrices.asFuncMatrix(LinearFunc.getInstance(destRange, srcRange), destType, matrix); } this.suitableWholeSlide = matrix; this.scaleX = (double) suitableWholeSlide.dim(DIM_WIDTH) / (double) levelDimX; this.scaleY = (double) suitableWholeSlide.dim(DIM_HEIGHT) / (double) levelDimY; } public double scaleX() { return scaleX; } public double scaleY() { return scaleY; } public Matrix scaleWholeSlide(IRectangularArea area) { return scaleWholeSlide(area.min(0), area.min(1), area.max(0) + 1, area.max(1) + 1); } public Matrix scaleWholeSlide( final long fromX, final long fromY, final long toX, final long toY) { long t1 = System.nanoTime(); this.resultBackground = newResultMatrix(toX - fromX, toY - fromY); if (fromX == toX || fromY == toY) { return resultBackground; } fromXAtSlide = Math.round(fromX * scaleX); fromYAtSlide = Math.round(fromY * scaleY); toXAtSlide = Math.round(toX * scaleX); toYAtSlide = Math.round(toY * scaleY); Matrix subMatrixAtWholeSlide = suitableWholeSlide.subMatrix( 0, fromXAtSlide, fromYAtSlide, bandCount(), toXAtSlide, toYAtSlide, Matrix.ContinuationMode.ZERO_CONSTANT); // - Continuation is necessary for a case of calling this method for area outside the whole slide. // The background outside the whole image stays undefined (zero). if (isSkipCoarseData()) { fillBySkippingFiller(this.resultBackground, false); } else { Matrices.resize(null, Matrices.ResizingMethod.SIMPLE, this.resultBackground, subMatrixAtWholeSlide); // null argument forces usage of all CPU kernels } long t2 = System.nanoTime(); LOG.log(System.Logger.Level.DEBUG, () -> String.format(Locale.US, "Scaling background %s %d..%dx%d..%d (%d x %d), " + "at whole-slide pyramid: level %d, %d..%dx%d..%d (%d x %d): %.3f ms, %.3f MB/sec)", isSkipCoarseData() ? "(filling)" : "(scaling whole-slide image)", fromX, toX, fromY, toY, toX - fromX, toY - fromY, suitableWholeSlideLevel, fromXAtSlide, toXAtSlide, fromYAtSlide, toYAtSlide, toXAtSlide - fromXAtSlide, toYAtSlide - fromYAtSlide, (t2 - t1) * 1e-6, Matrices.sizeOf(this.resultBackground) / 1048576.0 / ((t2 - t1) * 1e-9) )); this.scalingTime += t2 - t1; return resultBackground; } public Matrix resultBackground() { return resultBackground; } public boolean done() { return resultBackground != null; } public long scalingTime() { return scalingTime; } } private static final class TileCacheIndex { final int resolutionLevel; final IRectangularArea tile; private TileCacheIndex(int resolutionLevel, IRectangularArea tile) { assert tile != null; this.resolutionLevel = resolutionLevel; this.tile = tile; } @Override public boolean equals(Object o) { if (this == o) { return true; } if (!(o instanceof TileCacheIndex)) { return false; } TileCacheIndex that = (TileCacheIndex) o; return resolutionLevel == that.resolutionLevel && tile.equals(that.tile); } @Override public int hashCode() { int result = resolutionLevel; result = 31 * result + tile.hashCode(); return result; } } private class MapOrLabelParallelReader implements Runnable { final SpecialImageKind kind; Matrix data = null; long readingTime = 0; long rotatingTime = 0; long correctingBandCountTime = 0; long correctingBitDepthTime = 0; private MapOrLabelParallelReader(SpecialImageKind kind) { this.kind = kind; } @Override public void run() { long t1 = System.nanoTime(); data = PlanePyramidTools.readSpecialOrSmallestMatrix(AbstractPlanePyramidSource.this, kind); long t2 = System.nanoTime(); if (data.elementType() != byte.class) { final Matrix newData = Arrays.SMM.newByteMatrix(data.dimensions()); final Range srcRange = Range.valueOf(0.0, data.array().maxPossibleValue(1.0)); final Range destRange = Range.valueOf(0.0, newData.array().maxPossibleValue(1.0)); Matrices.applyFunc(null, LinearFunc.getInstance(destRange, srcRange), newData, data); data = newData; } long t3 = System.nanoTime(); if (kind == SpecialImageKind.LABEL_ONLY_IMAGE) { data = rotateLabelImage(data); } long t4 = System.nanoTime(); final int requiredBandCount = bandCount(); if (data.dim(DIM_BAND) != requiredBandCount) { final Matrix newData = Arrays.SMM.newMatrix( UpdatablePArray.class, data.elementType(), requiredBandCount, data.dim(DIM_WIDTH), data.dim(DIM_HEIGHT)); Matrices.resize(null, Matrices.ResizingMethod.SIMPLE, newData, data); // - really it is not geometrical resizing, it is only the changing // the number of color bands in the coordinate #0 data = newData; } long t5 = System.nanoTime(); this.readingTime += t2 - t1; this.correctingBitDepthTime += t3 - t2; this.rotatingTime += t4 - t3; this.correctingBandCountTime += t5 - t4; } } private static class TileCache { final int tileDim; final long tileCachingMemory; final TileCacheHashMap tileCacheHashMap; private TileCache(int tileDim, long tileCachingMemory) { this.tileDim = tileDim; this.tileCachingMemory = tileCachingMemory; this.tileCacheHashMap = new TileCacheHashMap(); LOG.log(System.Logger.Level.DEBUG, () -> String.format(Locale.US, AbstractPlanePyramidSource.class.getSimpleName() + " is creating tile cache for tiles %dx%d, memory limit %.2f MB", tileDim, tileDim, tileCachingMemory / 1048576.0 )); } Matrix getTile(int resolutionLevel, IRectangularArea tile) { Matrix result = tileCacheHashMap.get(new TileCacheIndex(resolutionLevel, tile)); LOG.log(System.Logger.Level.DEBUG, () -> String.format( " " + AbstractPlanePyramidSource.class.getSimpleName() + " has " + (result != null ? "loaded data from the cache" : "NOT FOUND data in the cache") + " (level %d): %s", resolutionLevel, tile )); return result; } void putTile(int resolutionLevel, IRectangularArea tile, Matrix matrix) { Matrix prev = tileCacheHashMap.put(new TileCacheIndex(resolutionLevel, tile), matrix); if (prev == null) { LOG.log(System.Logger.Level.TRACE, () -> String.format( " " + AbstractPlanePyramidSource.class.getSimpleName() + " has stored data in the cache (level %d): %s", resolutionLevel, tile )); } } private class TileCacheHashMap extends LinkedHashMap> { private TileCacheHashMap() { super(16, 0.75f, true); } @Override protected boolean removeEldestEntry(Map.Entry> eldest) { boolean result = usedMemory() > tileCachingMemory; if (result) { LOG.log(System.Logger.Level.DEBUG, () -> AbstractPlanePyramidSource.class.getSimpleName() + " will remove the eldest entry from the cache"); } return result; } private double usedMemory() { double sum = 0.0; for (Matrix m : values()) { sum += Matrices.sizeOf(m); } return sum; } } } private static class SpeedInfo { double totalMemory = 0.0; double elapsedTime = 0.0; public String update(long memory, long time) { final String result; synchronized (this) { totalMemory += memory; elapsedTime += time; final long t = System.currentTimeMillis(); result = String.format(Locale.US, "%.1f MB / %.3f sec = %.3f MB/sec", totalMemory / 1048576.0, elapsedTime * 1e-9, totalMemory / 1048576.0 / (elapsedTime * 1e-9)); } return result; } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy