/* * License: GPL * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License 2 * as published by the Free Software Foundation. * * This program 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 this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ package org.janelia.alignment.spec; import java.awt.Rectangle; import java.awt.geom.Rectangle2D; import java.io.IOException; import java.io.Reader; import java.io.Serializable; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; import java.util.TreeMap; import org.janelia.alignment.ImageAndMask; import org.janelia.alignment.RenderParameters; import org.janelia.alignment.json.JsonUtils; import org.janelia.alignment.spec.stack.MipmapPathBuilder; import com.fasterxml.jackson.annotation.JsonIgnore; import mpicbg.models.CoordinateTransform; import mpicbg.models.CoordinateTransformList; import mpicbg.models.CoordinateTransformMesh; import mpicbg.models.NoninvertibleModelException; import mpicbg.trakem2.transform.TransformMesh; /** * Specifies a set of mipmap level images and masks along with * a list of transformations to perform against them. * * @author Stephan Saalfeld <[email protected]> */ public class TileSpec implements Serializable { private String tileId; private LayoutData layout; private String groupId; private Double z; private Double minX; private Double minY; private Double maxX; private Double maxY; private Double width; private Double height; @SuppressWarnings("unused") // older JSON specs without channels might have minIntensity explicitly specified private Double minIntensity; @SuppressWarnings("unused") // older JSON specs without channels might have maxIntensity explicitly specified private Double maxIntensity; private TreeMap<Integer, ImageAndMask> mipmapLevels; private List<ChannelSpec> channels; private MipmapPathBuilder mipmapPathBuilder; private ListTransformSpec transforms; private double meshCellSize = RenderParameters.DEFAULT_MESH_CELL_SIZE; public TileSpec() { this.mipmapLevels = new TreeMap<>(); this.transforms = new ListTransformSpec(); } public String getTileId() { return tileId; } public void setTileId(final String tileId) { this.tileId = tileId; } public LayoutData getLayout() { return layout; } public void setLayout(final LayoutData layout) { this.layout = layout; } public String getGroupId() { return groupId; } @SuppressWarnings("UnusedDeclaration") public void setGroupId(final String groupId) { this.groupId = groupId; } public Double getZ() { return z; } public void setZ(final Double z) { this.z = z; } @JsonIgnore public String getSectionId() { return layout == null ? null : layout.getSectionId(); } public Double getMinX() { return minX; } public Double getMinY() { return minY; } public Double getMaxX() { return maxX; } public Double getMaxY() { return maxY; } public double getMeshCellSize() { return meshCellSize; } public boolean isBoundingBoxDefined(final double meshCellSize) { return (this.meshCellSize == meshCellSize) && (minX != null) && (minY != null) && (maxX != null) && (maxY != null); } /** * The bounding box is only valid for a given meshCellSize, i.e. setting it * independently of the meshCellSize is potentially harmful. * * @param box coordinates that define the bounding box for this tile. */ public void setBoundingBox(final Rectangle box, final double meshCellSize) { this.minX = box.getX(); this.minY = box.getY(); this.maxX = box.getMaxX(); this.maxY = box.getMaxY(); this.meshCellSize = meshCellSize; } public TileBounds toTileBounds() { return new TileBounds(tileId, getSectionId(), z, minX, minY, maxX, maxY); } public int getNumberOfTrianglesCoveringWidth(final double meshCellSize) { return (int) (width / meshCellSize + 0.5); } /** * @return a transform mesh built from this spec's list of transforms. * * @throws IllegalStateException * if width or height have not been defined for this tile. */ public TransformMesh getTransformMesh(final double meshCellSize) throws IllegalStateException { if (! hasWidthAndHeightDefined()) { throw new IllegalStateException("width and height must be set to create transform mesh"); } final CoordinateTransformList<CoordinateTransform> ctList = getTransformList(); return new TransformMesh(ctList, getNumberOfTrianglesCoveringWidth(meshCellSize), width, height); } /** * @return a coordinate transform mesh built from this spec's list of transforms. * * @throws IllegalStateException * if width or height have not been defined for this tile. */ public CoordinateTransformMesh getCoordinateTransformMesh(final double meshCellSize) throws IllegalStateException { if (! hasWidthAndHeightDefined()) { throw new IllegalStateException("width and height must be set to create transform mesh"); } final CoordinateTransformList<CoordinateTransform> ctList = getTransformList(); return new CoordinateTransformMesh(ctList, getNumberOfTrianglesCoveringWidth(meshCellSize), width, height); } /** * Derives this tile's bounding box attributes. * * @param force if true, attributes will always be derived; * otherwise attributes will only be derived if they do not already exist. * * @throws IllegalStateException * if width or height have not been defined for this tile. */ public void deriveBoundingBox(final double meshCellSize, final boolean force, final boolean sloppy) throws IllegalStateException { if (force || (!isBoundingBoxDefined(meshCellSize))) { if (sloppy) { if (! hasWidthAndHeightDefined()) { throw new IllegalStateException("width and height must be set to create a bounding box"); } final CoordinateTransformList<CoordinateTransform> ctList = getTransformList(); final ArrayList<double[]> borderSamples = new ArrayList<>(); /* top and bottom */ final int numXs = (int)Math.max(2, Math.round(width / meshCellSize)); final double dx = (width - 1) / (numXs - 1); for (double xi =0; xi < numXs; ++xi) { final double x = xi * dx; borderSamples.add(new double[]{x, 0}); borderSamples.add(new double[]{x, height}); } /* left and right */ final int numYs = (int)Math.max(2, Math.round(height / meshCellSize)); final double dy = (height - 1) / (numYs - 1); for (double yi =0; yi < numYs; ++yi) { final double y = yi * dy; borderSamples.add(new double[]{0, y}); borderSamples.add(new double[]{width, y}); } double xMin = Double.MAX_VALUE; double yMin = Double.MAX_VALUE; double xMax = -Double.MAX_VALUE; double yMax = -Double.MAX_VALUE; for (final double[] point : borderSamples) { ctList.applyInPlace(point); if ( point[ 0 ] < xMin ) xMin = point[ 0 ]; if ( point[ 0 ] > xMax ) xMax = point[ 0 ]; if ( point[ 1 ] < yMin ) yMin = point[ 1 ]; if ( point[ 1 ] > yMax ) yMax = point[ 1 ]; } setBoundingBox(new Rectangle((int)xMin, (int)yMin, (int)Math.ceil(xMax - xMin), (int)Math.ceil(yMax - yMin)), meshCellSize); // setBoundingBox(new Rectangle((int)xMin, (int)yMin, (int)(xMax - xMin), (int)(yMax - yMin)), meshCellSize); } else { final TransformMesh mesh = getTransformMesh(meshCellSize); setBoundingBox(mesh.getBoundingBox(), meshCellSize); } } } /** * Derives this tile's bounding box attributes. * * @param force if true, attributes will always be derived; * otherwise attributes will only be derived if they do not already exist. * * @throws IllegalStateException * if width or height have not been defined for this tile. */ public void deriveBoundingBox(final double meshCellSize, final boolean force) throws IllegalStateException { deriveBoundingBox(meshCellSize, force, true); } /** * @param tileSpecs collection of tile specs. * @param meshCellSize specifies the resolution to estimate the individual bounding boxes. * @param force if true, attributes will always be derived; * otherwise attributes will only be derived if they do not already exist. * @param preallocated optional pre-allocated bounding box instance to use for result. * * @return the bounding box of a collection of {@link TileSpec}s. * The returned bounding box is the union rectangle of all tiles individual bounding boxes. */ public static Rectangle2D.Double deriveBoundingBox( final Iterable<TileSpec> tileSpecs, final double meshCellSize, final boolean force, final Rectangle2D.Double preallocated) throws IllegalStateException { final double[] min = new double[] { Double.MAX_VALUE, Double.MAX_VALUE }; final double[] max = new double[] { -Double.MAX_VALUE, -Double.MAX_VALUE }; for (final TileSpec t : tileSpecs) { t.deriveBoundingBox(meshCellSize, force); final double tMinX = t.getMinX(); final double tMinY = t.getMinY(); final double tMaxX = t.getMaxX(); final double tMaxY = t.getMaxY(); if (min[0] > tMinX) min[0] = tMinX; if (min[1] > tMinY) min[1] = tMinY; if (max[0] < tMaxX) max[0] = tMaxX; if (max[1] < tMaxY) max[1] = tMaxY; } final Rectangle2D.Double box; if (preallocated == null) box = new Rectangle2D.Double( min[0], min[1], max[0] - min[0], max[1] - min[1]); else { box = preallocated; box.setRect(min[0], min[1], max[0] - min[0], max[1] - max[1]); } return box; } /** * @param x local x coordinate to transform into world coordinate. * @param y local y coordinate to transform into world coordinate. * * @return world coordinates (x, y, z) for the specified local coordinates. */ public double[] getWorldCoordinates(final double x, final double y) { final double[] worldCoordinates; final double[] w = new double[] {x, y}; if (hasTransforms()) { final CoordinateTransformList<CoordinateTransform> ctl = getTransformList(); ctl.applyInPlace(w); } if (z == null) { worldCoordinates = w; } else { worldCoordinates = new double[]{w[0], w[1], z}; } return worldCoordinates; } /** * @param x world x coordinate to inversely transform into local coordinate. * @param y world y coordinate to inversely transform into local coordinate. * * @return local coordinates (x, y, z) for the specified world coordinates. * * @throws IllegalStateException * if width or height have not been defined for this tile. * * @throws NoninvertibleModelException * if this tile's transforms cannot be inverted for the specified point. */ public double[] getLocalCoordinates(final double x, final double y, final double meshCellSize) throws IllegalStateException, NoninvertibleModelException { final double[] localCoordinates; final double[] l = new double[] {x, y}; if (hasTransforms()) { final CoordinateTransformMesh mesh = getCoordinateTransformMesh(meshCellSize); mesh.applyInverseInPlace(l); } if (z == null) { localCoordinates = l; } else { localCoordinates = new double[]{l[0], l[1], z}; } return localCoordinates; } public boolean hasWidthAndHeightDefined() { return ((width != null) && (height != null)); } public int getWidth() { int value = -1; if (width != null) { value = width.intValue(); } return value; } public void setWidth(final Double width) { this.width = width; } public int getHeight() { int value = -1; if (height != null) { value = height.intValue(); } return value; } public void setHeight(final Double height) { this.height = height; } @JsonIgnore public List<ChannelSpec> getAllChannels() { final List<ChannelSpec> channelList; if ((channels == null) || (channels.size() == 0)) { channelList = Collections.singletonList(new ChannelSpec(null, minIntensity, maxIntensity, mipmapLevels, mipmapPathBuilder)); } else { channelList = channels; } return channelList; } public List<ChannelSpec> getChannels(final Set<String> withNames) { final List<ChannelSpec> channelList = new ArrayList<>(); if ((channels == null) || (channels.size() == 0)) { if (withNames.contains(null)) { channelList.add(new ChannelSpec(null, minIntensity, maxIntensity, mipmapLevels, mipmapPathBuilder)); } } else { for (final ChannelSpec channelSpec : channels) { if (withNames.contains(channelSpec.getName())) { channelList.add(channelSpec); } } } return channelList; } public String getFirstChannelName() { String firstChannelName = null; final List<ChannelSpec> channelSpecs = getAllChannels(); if (channelSpecs.size() > 0) { firstChannelName = channelSpecs.get(0).getName(); } return firstChannelName; } /** * Converts legacy single channel tile spec mipmap data to a channel spec with the specified name. * * @param channelName name of the single channel. * * @throws IllegalStateException * if this tile spec already has defined channels. */ public void convertLegacyToChannel(final String channelName) throws IllegalStateException { if (channels == null) { channels = new ArrayList<>(); channels.add(new ChannelSpec(channelName, minIntensity, maxIntensity, mipmapLevels, mipmapPathBuilder)); this.minIntensity = null; this.maxIntensity = null; this.mipmapLevels = null; this.mipmapPathBuilder = null; } else { throw new IllegalStateException("channels already defined"); } } public void addChannel(final ChannelSpec channelSpec) { if (channels == null) { channels = new ArrayList<>(); } channels.add(channelSpec); } @JsonIgnore public Map.Entry<Integer, ImageAndMask> getFirstMipmapEntry() { final Map.Entry<Integer, ImageAndMask> firstEntry; if ((channels == null) || (channels.size() == 0)) { firstEntry = mipmapLevels.firstEntry(); } else { firstEntry = channels.get(0).getFirstMipmapEntry(); } return firstEntry; } public void setMinAndMaxIntensity(final double minIntensity, final double maxIntensity, final String forChannelName) { if (channels == null) { this.minIntensity = minIntensity; this.maxIntensity = maxIntensity; } else { if (forChannelName == null) { for (final ChannelSpec channelSpec : channels) { if (channelSpec.getName() == null) { channelSpec.setMinAndMaxIntensity(minIntensity, maxIntensity); break; } } } else { for (final ChannelSpec channelSpec : channels) { if (forChannelName.equals(channelSpec.getName())) { channelSpec.setMinAndMaxIntensity(minIntensity, maxIntensity); break; } } } } } public void setMipmapPathBuilder(final MipmapPathBuilder mipmapPathBuilder) { this.mipmapPathBuilder = mipmapPathBuilder; if (channels != null) { for (final ChannelSpec channelSpec : channels) { channelSpec.setMipmapPathBuilder(mipmapPathBuilder); } } } public boolean hasTransforms() { return ((transforms != null) && (transforms.size() > 0)); } public boolean hasTransformWithLabel(final String label) { boolean hasLabel = false; if (transforms != null) { hasLabel = transforms.hasLabel(label); } return hasLabel; } /** * @return true if this tile spec has at least one channel with a mask. */ public boolean hasMasks() { boolean hasMasks = false; for (final ChannelSpec channelSpec : getAllChannels()) { if (channelSpec.hasMask()) { hasMasks = true; break; } } return hasMasks; } public ListTransformSpec getTransforms() { return transforms; } @JsonIgnore public TransformSpec getLastTransform() { TransformSpec lastTransform = null; if (hasTransforms()) { lastTransform = transforms.getLastSpec(); } return lastTransform; } public void setTransforms(final ListTransformSpec transforms) { this.transforms = transforms; } public void addTransformSpecs(final List<TransformSpec> transformSpecs) { transforms.addAllSpecs(transformSpecs); } public void removeLastTransformSpec() { transforms.removeLastSpec(); } /** * Replace this tile's possibly nested transform list with a flattened version. */ public void flattenTransforms() { final ListTransformSpec flattenedList = new ListTransformSpec(); transforms.flatten(flattenedList); transforms = flattenedList; } /** * Replace this tile's nested transform list with a flattened version * and optionally filter/exclude labelled transforms. * * @param excludeAll if true, removes all transforms. * Specify as null to skip. * * @param excludeAfterLastLabels removes all transforms after the last occurrence * of a transform with one of these labels. * Specify as null to skip. * * @param excludeFirstAndAllAfterLabels removes the first transform with one of these labels. * Specify as null to skip. */ public void flattenAndFilterTransforms(final Boolean excludeAll, final Set<String> excludeAfterLastLabels, final Set<String> excludeFirstAndAllAfterLabels) { if ((excludeAll != null) && excludeAll) { transforms = new ListTransformSpec(); } else { transforms = transforms.flattenAndFilter(excludeAfterLastLabels, excludeFirstAndAllAfterLabels); } } /** * @throws IllegalArgumentException * if this spec's mipmaps are invalid. */ public void validateMipmaps() throws IllegalArgumentException { for (final ChannelSpec channelSpec : getAllChannels()) { channelSpec.validateMipmaps(tileId); } } /** * @throws IllegalArgumentException * if this specification is invalid. */ public void validate() throws IllegalArgumentException { validateMipmaps(); transforms.validate(); } /** * Get a copy of this {@link TileSpec}'s transforms as a {@link CoordinateTransformList}. * If this {@link TileSpec} does not have any transforms, an empty list is returned. * * The returned list is no longer cached, so it can be used/changed safely without affecting this {@link TileSpec}. * * @return transform list copy for this tile spec. * * @throws IllegalArgumentException * if the list cannot be generated. */ @JsonIgnore public CoordinateTransformList<CoordinateTransform> getTransformList() throws IllegalArgumentException { final CoordinateTransformList<CoordinateTransform> ctl; if (transforms == null) { ctl = new CoordinateTransformList<>(); } else { ctl = transforms.getNewInstanceAsList(); } return ctl; } @Override public String toString() { return tileId; } public String toJson() { return JSON_HELPER.toJson(this); } public static TileSpec fromJson(final String json) { return JSON_HELPER.fromJson(json); } public static List<TileSpec> fromJsonArray(final String json) { // TODO: verify using Arrays.asList optimization is actually faster // return JSON_HELPER.fromJsonArray(json); try { return Arrays.asList(JsonUtils.MAPPER.readValue(json, TileSpec[].class)); } catch (final IOException e) { throw new IllegalArgumentException(e); } } public static List<TileSpec> fromJsonArray(final Reader json) throws IOException { // TODO: verify using Arrays.asList optimization is actually faster // return JSON_HELPER.fromJsonArray(json); try { return Arrays.asList(JsonUtils.MAPPER.readValue(json, TileSpec[].class)); } catch (final IOException e) { throw new IllegalArgumentException(e); } } private static final JsonUtils.Helper<TileSpec> JSON_HELPER = new JsonUtils.Helper<>(TileSpec.class); }