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

nom.tam.image.StandardImageTiler Maven / Gradle / Ivy

package nom.tam.image;

import java.io.EOFException;

/*
 * #%L
 * nom.tam FITS library
 * %%
 * Copyright (C) 2004 - 2024 nom-tam-fits
 * %%
 * This is free and unencumbered software released into the public domain.
 *
 * Anyone is free to copy, modify, publish, use, compile, sell, or
 * distribute this software, either in source code form or as a compiled
 * binary, for any purpose, commercial or non-commercial, and by any
 * means.
 *
 * In jurisdictions that recognize copyright laws, the author or authors
 * of this software dedicate any and all copyright interest in the
 * software to the public domain. We make this dedication for the benefit
 * of the public at large and to the detriment of our heirs and
 * successors. We intend this dedication to be an overt act of
 * relinquishment in perpetuity of all present and future rights to this
 * software under copyright law.
 *
 * 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 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.
 * #L%
 */

import java.io.IOException;
import java.lang.reflect.Array;
import java.util.Arrays;

import nom.tam.util.ArrayDataOutput;
import nom.tam.util.ArrayFuncs;
import nom.tam.util.RandomAccess;
import nom.tam.util.type.ElementType;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;

/**
 * 

* Standard image tiling implementation. FITS tiles are always 2-dimentional, but really images of any dimensions may be * covered with such tiles. *

*

* Modified May 2, 2000 by T. McGlynn to permit tiles that go off the edge of the image. *

*/ public abstract class StandardImageTiler implements ImageTiler { /** * @param dims The dimensions of the array. * @param pos The index requested. * * @return the offset of a given position. */ public static long getOffset(int[] dims, int[] pos) { long offset = 0; for (int i = 0; i < dims.length; i++) { if (i > 0) { offset *= dims[i]; } offset += pos[i]; } return offset; } /** * Increment the offset within the position array. Note that we never look at the last index since we copy data a * block at a time and not byte by byte. * * @param start The starting corner values. * @param current The current offsets. * @param lengths The desired dimensions of the subset. * * @return true if the current array was changed */ protected static boolean incrementPosition(int[] start, int[] current, int[] lengths) { final int[] steps = new int[start.length]; Arrays.fill(steps, 1); return StandardImageTiler.incrementPosition(start, current, lengths, steps); } /** * Increment the offset within the position array. Note that we never look at the last index since we copy data a * block at a time and not byte by byte. * * @param start The starting corner values. * @param current The current offsets. * @param lengths The desired dimensions of the subset. * @param steps The desired number of steps to take until the next position. * * @return true if the current array was changed */ protected static boolean incrementPosition(int[] start, int[] current, int[] lengths, int[] steps) { for (int i = start.length - 2; i >= 0; i--) { if (current[i] - start[i] < lengths[i] - steps[i]) { current[i] += steps[i]; if (start.length - 1 - (i + 1) >= 0) { System.arraycopy(start, i + 1, current, i + 1, start.length - 1 - (i + 1)); } return true; } } return false; } private final RandomAccess randomAccessFile; private final long fileOffset; private final int[] dims; private final Class base; /** * Create a tiler. * * @param f The random access device from which image data may be read. This may be null if the tile * information is available from memory. * @param fileOffset The file offset within the RandomAccess device at which the data begins. * @param dims The actual dimensions of the image. * @param base The base class (should be a primitive type) of the image. */ @SuppressFBWarnings(value = "EI_EXPOSE_REP", justification = "intended exposure of mutable data") public StandardImageTiler(RandomAccess f, long fileOffset, int[] dims, Class base) { randomAccessFile = f; this.fileOffset = fileOffset; this.dims = dims; this.base = base; } /** * File a tile segment from a file using a default value for striding. * * @param output The output to send data. This can be an ArrayDataOutput to stream data to and prevent memory * consumption of a tile being in memory. * @param delta The offset from the beginning of the image in bytes. * @param outputOffset The index into the output array. * @param segment The number of elements to be read for this segment. * * @throws IOException if the underlying stream failed */ @SuppressFBWarnings(value = "RR_NOT_CHECKED", justification = "this read will never return less than the requested length") protected void fillFileData(Object output, long delta, int outputOffset, int segment) throws IOException { fillFileData(output, delta, outputOffset, segment, 1); } /** * File a tile segment from a file, jumping each step number of values to the next read. * * @param output The output to send data. This can be an ArrayDataOutput to stream data to and prevent memory * consumption of a tile being in memory. * @param delta The offset from the beginning of the image in bytes. * @param outputOffset The index into the output array. * @param segment The number of elements to be read for this segment. * @param step The number of jumps until the next read. Only works for streaming out data. * * @throws EOFException if already at the end of file / stream * @throws IOException if the underlying stream failed */ protected void fillFileData(Object output, long delta, int outputOffset, int segment, int step) throws IOException { if (output instanceof ArrayDataOutput) { this.fillFileData((ArrayDataOutput) output, delta, segment, step); } else { randomAccessFile.seek(fileOffset + delta); int got = 0; if (base == float.class) { got = randomAccessFile.read((float[]) output, outputOffset, segment); } else if (base == int.class) { got = randomAccessFile.read((int[]) output, outputOffset, segment); } else if (base == short.class) { got = randomAccessFile.read((short[]) output, outputOffset, segment); } else if (base == double.class) { got = randomAccessFile.read((double[]) output, outputOffset, segment); } else if (base == byte.class) { got = randomAccessFile.read((byte[]) output, outputOffset, segment); } else if (base == long.class) { got = randomAccessFile.read((long[]) output, outputOffset, segment); } else { throw new IOException("Invalid type for tile array"); } if (got < 0) { throw new EOFException(); } } } /** * File a tile segment from a file into the given stream. This will deal only with bytes to avoid having to check * the base type and calling a specific method. Converting the base type to a byte is a simple multiplication * operation anyway. Uses a default value for striding (1). * * @param output The output stream. * @param delta The offset from the beginning of the image in bytes. * @param segment The number of elements to be read for this segment. * * @throws IOException if the underlying stream failed */ @SuppressFBWarnings(value = "RR_NOT_CHECKED", justification = "this read will never return less than the requested length") protected void fillFileData(ArrayDataOutput output, long delta, int segment) throws IOException { fillFileData(output, delta, segment, 1); } /** * File a tile segment from a file into the given stream. This will deal only with bytes to avoid having to check * the base type and calling a specific method. Converting the base type to a byte is a simple multiplication * operation anyway. * * @param output The output stream. * @param delta The offset from the beginning of the image in bytes. * @param segment The number of elements to be read for this segment. * @param step The number of elements until the next read. * * @throws IOException if the underlying stream failed * * @since 1.18 */ @SuppressFBWarnings(value = "RR_NOT_CHECKED", justification = "this read will never return less than the requested length") protected void fillFileData(ArrayDataOutput output, long delta, int segment, int step) throws IOException { final int byteSize = ElementType.forClass(base).size(); // Subtract one from the step since when we read from a stream, an actual // "step" only exists if it's greater // than 1. final int stepSize = (step - 1) * byteSize; randomAccessFile.seek(fileOffset + delta); // One value at a time final byte[] buffer = new byte[byteSize]; long seekOffset = randomAccessFile.position(); int bytesRead = 0; // This is the byte count that will be read. final int expectedBytes = segment * byteSize; while (bytesRead < expectedBytes) { // Prepare for the next read by seeking to the next step randomAccessFile.seek(seekOffset); final int currReadByteCount = randomAccessFile.read(buffer, 0, buffer.length); // Stop if there is no more to read. if (currReadByteCount < 0) { break; } output.write(buffer, 0, currReadByteCount); seekOffset = randomAccessFile.position() + stepSize; bytesRead += currReadByteCount + stepSize; } output.flush(); } /** * Fill a single segment from memory. This routine is called recursively to handle multidimensional arrays. E.g., if * data is three-dimensional, this will recurse two levels until we get a call with a single dimensional datum. At * that point the appropriate data will be copied into the output. Uses a default value for striding (1). * * @param data The in-memory image data. * @param posits The current position for which data is requested. * @param length The size of the segments. * @param output The output tile. * @param outputOffset The current offset into the output tile. * @param dim The current dimension being * * @throws IOException If the output is a stream and there is an I/O error. */ protected void fillMemData(Object data, int[] posits, int length, Object output, int outputOffset, int dim) throws IOException { fillMemData(data, posits, length, output, outputOffset, dim, 1); } /** * Fill a single segment from memory. This routine is called recursively to handle multidimensional arrays. E.g., if * data is three-dimensional, this will recurse two levels until we get a call with a single dimensional datum. At * that point the appropriate data will be copied into the output, jumping the number of step values. * * @param data The in-memory image data. * @param posits The current position for which data is requested. * @param length The size of the segments. * @param output The output tile. * @param outputOffset The current offset into the output tile. * @param dim The current dimension being * @param step The number of jumps to the next value. * * @throws IOException If the output is a stream and there is an I/O error. * * @since 1.18 */ protected void fillMemData(Object data, int[] posits, int length, Object output, int outputOffset, int dim, int step) throws IOException { if (data instanceof Object[]) { Object[] xo = (Object[]) data; fillMemData(xo[posits[dim]], posits, length, output, outputOffset, dim + 1, step); } else { // Adjust the spacing for the actual copy. int startFrom = posits[dim]; int startTo = outputOffset; int copyLength = length; if (posits[dim] < 0) { startFrom -= posits[dim]; startTo -= posits[dim]; copyLength += posits[dim]; } if (posits[dim] + length > dims[dim]) { copyLength -= posits[dim] + length - dims[dim]; } if (output instanceof ArrayDataOutput) { // Intentionally missing char and boolean here as they are not // valid BITPIX values. final ArrayDataOutput arrayDataOutput = ((ArrayDataOutput) output); for (int i = startFrom; i < startFrom + copyLength; i += step) { if (base == float.class) { arrayDataOutput.writeFloat(Array.getFloat(data, i)); } else if (base == int.class) { arrayDataOutput.writeInt(Array.getInt(data, i)); } else if (base == double.class) { arrayDataOutput.writeDouble(Array.getDouble(data, i)); } else if (base == long.class) { arrayDataOutput.writeLong(Array.getLong(data, i)); } else if (base == short.class) { arrayDataOutput.writeShort(Array.getShort(data, i)); } else if (base == byte.class) { arrayDataOutput.writeByte(Array.getByte(data, i)); } } arrayDataOutput.flush(); } else { ArrayFuncs.copy(data, startFrom, output, startTo, copyLength, step); } } } /** * Fill the subset using a default value for striding. * * @param data The memory-resident data image. This may be null if the image is to be read from a file. This * should be a multidimensional primitive array. * @param o The tile to be filled. This is a simple primitive array, or an ArrayDataOutput instance. * @param newDims The dimensions of the full image. * @param corners The indices of the corner of the image. * @param lengths The dimensions of the subset. * * @throws IOException if the underlying stream failed */ protected void fillTile(Object data, Object o, int[] newDims, int[] corners, int[] lengths) throws IOException { final int[] steps = new int[corners.length]; Arrays.fill(steps, 1); fillTile(data, o, newDims, corners, lengths, steps); } /** * Fill the subset, jumping each step value to the next read. * * @param data The memory-resident data image. This may be null if the image is to be read from a file. This * should be a multidimensional primitive array. * @param o The tile to be filled. This is a simple primitive array, or an ArrayDataOutput instance. * @param newDims The dimensions of the full image. * @param corners The indices of the corner of the image. * @param lengths The dimensions of the subset. * @param steps The number of steps to take until the next read in each axis. * * @throws IOException if the underlying stream failed */ protected void fillTile(Object data, Object o, int[] newDims, int[] corners, int[] lengths, int[] steps) throws IOException { int n = newDims.length; int[] posits = new int[n]; final boolean isStreaming = (o instanceof ArrayDataOutput); // TODO: When streaming out to an ArrayDataOutput, use this tiler's base // class to determine the element size. // TODO: If that is not sufficient, then maybe it needs to be passed in? // TODO: jenkinsd 2022.12.21 // final int baseLength = isStreaming ? ElementType.forClass(base).size() : ArrayFuncs.getBaseLength(o); int segment = lengths[n - 1]; int segmentStep = steps[n - 1]; System.arraycopy(corners, 0, posits, 0, n); long currentOffset = 0; if (data == null) { currentOffset = randomAccessFile.getFilePointer(); } int outputOffset = 0; // Flag to indicate something was written out. This is only relevant if // the output is an ArrayDataOutput. boolean hasNoOverlap = true; do { // This implies there is some overlap // in the last index (in conjunction // with other tests) int mx = newDims.length - 1; boolean validSegment = posits[mx] + lengths[mx] >= 0 && posits[mx] < newDims[mx]; // Don't do anything for the current // segment if anything but the // last index is out of range. if (validSegment) { for (int i = 0; i < mx; i++) { if (posits[i] < 0 || posits[i] >= newDims[i]) { validSegment = false; break; } } } if (validSegment) { hasNoOverlap = false; if (data != null) { fillMemData(data, posits, segment, o, outputOffset, 0, segmentStep); } else { long offset = getOffset(newDims, posits) * baseLength; // Point to offset at real beginning // of segment int actualLen = segment; long actualOffset = offset; int actualOutput = outputOffset; if (posits[mx] < 0) { actualOffset -= (long) posits[mx] * baseLength; actualOutput -= posits[mx]; actualLen += posits[mx]; } if (posits[mx] + segment > newDims[mx]) { actualLen -= posits[mx] + segment - newDims[mx]; } fillFileData(o, actualOffset, actualOutput, actualLen, segmentStep); } } if (!isStreaming) { outputOffset += segment; } } while (incrementPosition(corners, posits, lengths, steps)); if (data == null) { randomAccessFile.seek(currentOffset); } if (isStreaming && hasNoOverlap) { throw new IOException("Sub-image not within image"); } } @Override public Object getCompleteImage() throws IOException { if (randomAccessFile == null) { throw new IOException("Attempt to read from null file"); } long currentOffset = randomAccessFile.getFilePointer(); Object o = ArrayFuncs.newInstance(base, dims); randomAccessFile.seek(fileOffset); randomAccessFile.readImage(o); randomAccessFile.seek(currentOffset); return o; } /** * See if we can get the image data from memory. This may be overridden by other classes, notably in * nom.tam.fits.ImageData. * * @return the image data */ protected abstract Object getMemoryImage(); @Override public Object getTile(int[] corners, int[] lengths) throws IOException { final int[] steps = new int[corners.length]; Arrays.fill(steps, 1); return getTile(corners, lengths, steps); } @Override public Object getTile(int[] corners, int[] lengths, int[] steps) throws IOException { if (corners.length != dims.length || lengths.length != dims.length) { throw new IOException("Inconsistent sub-image request"); } int arraySize = 1; for (int i = 0; i < dims.length; i++) { if (corners[i] < 0 || lengths[i] < 0 || corners[i] + lengths[i] > dims[i]) { throw new IOException("Sub-image not within image"); } if (steps[i] < 1) { throw new IOException("Step value cannot be less than 1."); } arraySize *= lengths[i]; } Object outArray = ArrayFuncs.newInstance(base, arraySize); getTile(outArray, corners, lengths, steps); return outArray; } @Override public void getTile(Object output, int[] corners, int[] lengths) throws IOException { final int[] steps = new int[corners.length]; Arrays.fill(steps, 1); this.getTile(output, corners, lengths, steps); } @Override public void getTile(Object output, int[] corners, int[] lengths, int[] steps) throws IOException { Object data = getMemoryImage(); if (data == null && randomAccessFile == null) { throw new IOException("No data source for tile subset"); } fillTile(data, output, dims, corners, lengths, steps); } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy