/* * $Id$ * * Copyright 2008-2013 University of Dundee. All rights reserved. * Use is subject to license terms supplied in LICENSE.txt */ package ome.services.projection; import java.io.IOException; import java.nio.ByteBuffer; import java.util.List; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.transaction.annotation.Transactional; import ome.annotations.RolesAllowed; import ome.api.IPixels; import ome.api.IProjection; import ome.api.ServiceInterface; import ome.conditions.ResourceError; import ome.conditions.ValidationException; import ome.io.nio.DimensionsOutOfBoundsException; import ome.io.nio.PixelBuffer; import ome.util.PixelData; import ome.io.nio.PixelsService; import ome.logic.AbstractLevel2Service; import ome.model.core.Channel; import ome.model.core.Image; import ome.model.core.Pixels; import ome.model.enums.PixelsType; import ome.model.stats.StatsInfo; /** * Implements projection functionality for Pixels sets as declared in {@link * IProjection}. * * @author Chris Allan      <a * href="mailto:callan@blackcat.ca">callan@blackcat.ca</a> * @since OMERO-Beta3.1 */ @Transactional(readOnly = true) public class ProjectionBean extends AbstractLevel2Service implements IProjection { /** The logger for this class. */ private static Logger log = LoggerFactory.getLogger(ProjectionBean.class); /** Reference to the service used to retrieve the pixels metadata. */ protected transient IPixels iPixels; /** Reference to the service used to retrieve the pixels data. */ protected transient PixelsService pixelsService; /** * Returns the interface this implementation is for. * @see AbstractLevel2Service#getServiceInterface() */ public Class<? extends ServiceInterface> getServiceInterface() { return IProjection.class; } /** * IPixels bean injector. For use during configuration. Can only be called * once. */ public void setIPixels(IPixels iPixels) { getBeanHelper().throwIfAlreadySet(this.iPixels, iPixels); this.iPixels = iPixels; } /** * PixelsService bean injector. For use during configuration. Can only be * called once. */ public void setPixelsService(PixelsService pixelsService) { getBeanHelper().throwIfAlreadySet(this.pixelsService, pixelsService); this.pixelsService = pixelsService; } /* (non-Javadoc) * @see ome.api.IProjection#projectStack(long, ome.model.enums.PixelsType, int, int, int, int, int, int) */ @RolesAllowed("user") public byte[] projectStack(long pixelsId, PixelsType pixelsType, int algorithm, int timepoint, int channelIndex, int stepping, int start, int end) { ProjectionContext ctx = new ProjectionContext(); ctx.pixels = iQuery.get(Pixels.class, pixelsId); PixelBuffer pixelBuffer = pixelsService.getPixelBuffer( ctx.pixels, false); zIntervalBoundsCheck(start, end, ctx.pixels.getSizeZ()); outOfBoundsStepping(stepping); outOfBoundsCheck(channelIndex, "channel"); outOfBoundsCheck(timepoint, "timepoint"); Integer v = ctx.pixels.getSizeT(); if (timepoint >= v) throw new ValidationException("timepoint must be <"+v); v = ctx.pixels.getSizeC(); if (channelIndex >= v) throw new ValidationException("channel index must be <"+v); try { if (pixelsType == null) { pixelsType = ctx.pixels.getPixelsType(); } else { pixelsType = iQuery.get(PixelsType.class, pixelsType.getId()); } ctx.planeSizeInPixels = ctx.pixels.getSizeX() * ctx.pixels.getSizeY(); int planeSize = ctx.planeSizeInPixels * (iPixels.getBitDepth(pixelsType) / 8); byte[] buf = new byte[planeSize]; ctx.from = pixelBuffer.getStack(channelIndex, timepoint); ctx.to = new PixelData(pixelsType.getValue(), ByteBuffer.wrap(buf)); switch (algorithm) { case IProjection.MAXIMUM_INTENSITY: { projectStackMax(ctx, stepping, start, end, false); break; } case IProjection.MEAN_INTENSITY: { projectStackMean(ctx, stepping, start, end, false); break; } case IProjection.SUM_INTENSITY: { projectStackSum(ctx, stepping, start, end, false); break; } default: { throw new IllegalArgumentException( "Unknown algorithm: " + algorithm); } } return buf; } catch (IOException e) { String error = String.format( "I/O error retrieving stack C=%d T=%d: %s", channelIndex, timepoint, e.getMessage()); log.error(error, e); throw new ResourceError(error); } catch (DimensionsOutOfBoundsException e) { String error = String.format( "C=%d or T=%d out of range for Pixels Id %d: %s", channelIndex, timepoint, ctx.pixels.getId(), e.getMessage()); log.error(error, e); throw new ValidationException(error); } finally { try { pixelBuffer.close(); } catch (IOException e) { log.error("Buffer did not close successfully.", e); throw new ResourceError( e.getMessage() + " Please check server log."); } if (ctx.from != null) { ctx.from.dispose(); } } } /* (non-Javadoc) * @see ome.api.IProjection#projectPixels(long, ome.model.enums.PixelsType, int, int, int, java.util.List, int, int, int, java.lang.String) */ @RolesAllowed("user") @Transactional(readOnly = false) public long projectPixels(long pixelsId, PixelsType pixelsType, int algorithm, int tStart, int tEnd, List<Integer> channels, int stepping, int zStart, int zEnd, String name) { // First, copy and resize our image with sizeZ = 1. ProjectionContext ctx = new ProjectionContext(); ctx.pixels = iQuery.get(Pixels.class, pixelsId); Image image = ctx.pixels.getImage(); name = name == null? image.getName() + " Projection" : name; //size of the new buffer. //Add control for z zIntervalBoundsCheck(zStart, zEnd, ctx.pixels.getSizeZ()); outOfBoundsStepping(stepping); Integer sizeT = tEnd-tStart+1; if (tStart > tEnd) sizeT = tStart-tEnd+1; if (sizeT <= 0) sizeT = null; //Channels and timepoint validation done there long newImageId = iPixels.copyAndResizeImage(image.getId(), null, null, 1, sizeT, channels, name, false); Image newImage = iQuery.get(Image.class, newImageId); Pixels newPixels = newImage.getPixels(0); if (pixelsType == null) { pixelsType = ctx.pixels.getPixelsType(); } else { pixelsType = iQuery.get(PixelsType.class, pixelsType.getId()); } newPixels.setPixelsType(pixelsType); // Project each stack for each channel and each timepoint in the // entire image, copying into the pixel buffer the projected pixels. PixelBuffer sourceBuffer = pixelsService.getPixelBuffer( ctx.pixels, false); try { PixelBuffer destinationBuffer = pixelsService.getPixelBuffer( newPixels, true); try { ctx.planeSizeInPixels = ctx.pixels.getSizeX() * ctx.pixels.getSizeY(); int planeSize = ctx.planeSizeInPixels * (iPixels.getBitDepth(pixelsType) / 8); byte[] buf = new byte[planeSize]; ctx.to = new PixelData(pixelsType.getValue(), ByteBuffer.wrap(buf)); int newC = 0; for (Integer c : channels) { ctx.minimum = Double.MAX_VALUE; ctx.maximum = Double.MIN_VALUE; for (int t = tStart; t <= tEnd; t++) { try { ctx.from = sourceBuffer.getStack(c, t); switch (algorithm) { case IProjection.MAXIMUM_INTENSITY: { projectStackMax(ctx, stepping, zStart, zEnd, true); break; } case IProjection.MEAN_INTENSITY: { projectStackMean(ctx, stepping, zStart, zEnd, true); break; } case IProjection.SUM_INTENSITY: { projectStackSum(ctx, stepping, zStart, zEnd, true); break; } default: { throw new IllegalArgumentException( "Unknown algorithm: " + algorithm); } } destinationBuffer.setPlane(buf, 0, newC, t); } catch (IOException e) { String error = String.format( "I/O error retrieving stack C=%d T=%d: %s", c, t, e.getMessage()); log.error(error, e); throw new ResourceError(error); } catch (DimensionsOutOfBoundsException e) { String error = String.format( "C=%d or T=%d out of range for Pixels Id %d: %s", c, t, ctx.pixels.getId(), e.getMessage()); log.error(error, e); throw new ValidationException(error); } finally { if (ctx.from != null) { ctx.from.dispose(); } } } // Handle the change of minimum and maximum for this channel. Channel channel = newPixels.getChannel(newC); StatsInfo si = new StatsInfo(); si.setGlobalMin(ctx.minimum); si.setGlobalMax(ctx.maximum); channel.setStatsInfo(si); // Set our methodology newPixels.setMethodology( IProjection.METHODOLOGY_STRINGS[algorithm]); newC++; } } finally { try { destinationBuffer.close(); } catch (IOException e) { log.error("Buffer did not close successfully: " + destinationBuffer , e); throw new ResourceError( e.getMessage() + " Please check server log."); } } } finally { try { sourceBuffer.close(); } catch (IOException e) { log.error("Buffer did not close successfully: " + sourceBuffer, e); throw new ResourceError( e.getMessage() + " Please check server log."); } } newImage = iUpdate.saveAndReturnObject(newImage); return newImage.getId(); } /** * Ensures that a particular dimension value is not out of range (ex. less * than zero). * @param value The value to check. * @param name The name of the value to be used for error reporting. * @throws ValidationException If <code>value</code> is out of range. */ private void outOfBoundsCheck(Integer value, String name) { if (value != null && value < 0) { throw new ValidationException(name + ": " + value + " < 0"); } } /** * Ensures that a particular dimension value is not out of range. * @param value The value to check. * @throws ValidationException If <code>value</code> is out of range. */ private void outOfBoundsStepping(Integer value) { if (value != null && value <= 0) { throw new ValidationException("stepping: " + value + " <= 0"); } } /** * Ensures that a particular dimension value is not out of range (ex. less * than zero). * @param start The lower bound of the interval. * @param end The upper bound of the interval. * @param name The name of the value to be used for error reporting. * @throws ValidationException If <code>value</code> is out of range. */ private void zIntervalBoundsCheck(int start, int end, Integer maxZ) { if (start < 0 || end < 0) throw new ValidationException("Z interval value cannot be negative."); if (start >= maxZ || end >= maxZ) throw new ValidationException("Z interval value cannot be >= "+maxZ); } /** * Projects a stack based on the maximum intensity at each XY coordinate. * @param ctx The context of our projection. * @param stepping Stepping value to use while calculating the projection. * For example, <code>stepping=1</code> will use every optical section from * <code>start</code> to <code>end</code> where <code>stepping=2</code> will * use every other section from <code>start</code> to <code>end</code> to * perform the projection. * @param start Optical section to start projecting from. * @param end Optical section to finish projecting. * @param doMinMax Whether or not to calculate the minimum and maximum of * the projected pixel data. */ private void projectStackMax(ProjectionContext ctx, int stepping, int start, int end, boolean doMinMax) { int currentPlaneStart; double projectedValue, stackValue; double minimum = ctx.minimum; double maximum = ctx.maximum; for (int i = 0; i < ctx.planeSizeInPixels; i++) { projectedValue = 0; for (int z = start; z <= end; z += stepping) { currentPlaneStart = ctx.planeSizeInPixels * z; stackValue = ctx.from.getPixelValue(currentPlaneStart + i); if (stackValue > projectedValue) { projectedValue = stackValue; } } ctx.to.setPixelValue(i, projectedValue); if (doMinMax) { minimum = projectedValue < minimum? projectedValue : minimum; maximum = projectedValue > maximum? projectedValue : maximum; } } ctx.minimum = minimum; ctx.maximum = maximum; } /** * Projects a stack based on the mean intensity at each XY coordinate. * @param from The raw pixel data from the stack to project from. * @param ctx The context of our projection. * source Pixels set pixels type will be used. * @param stepping Stepping value to use while calculating the projection. * For example, <code>stepping=1</code> will use every optical section from * <code>start</code> to <code>end</code> where <code>stepping=2</code> will * use every other section from <code>start</code> to <code>end</code> to * perform the projection. * @param start Optical section to start projecting from. * @param end Optical section to finish projecting. * @param doMinMax Whether or not to calculate the minimum and maximum of * the projected pixel data. */ private void projectStackMean(ProjectionContext ctx, int stepping, int start, int end, boolean doMinMax) { projectStackMeanOrSum(ctx, stepping, start, end, true, doMinMax); } /** * Projects a stack based on the sum intensity at each XY coordinate. * @param ctx The context of our projection. * @param pixelsType The destination Pixels type. If <code>null</code>, the * source Pixels set pixels type will be used. * @param stepping Stepping value to use while calculating the projection. * For example, <code>stepping=1</code> will use every optical section from * <code>start</code> to <code>end</code> where <code>stepping=2</code> will * use every other section from <code>start</code> to <code>end</code> to * perform the projection. * @param start Optical section to start projecting from. * @param end Optical section to finish projecting. * @param doMinMax Whether or not to calculate the minimum and maximum of * the projected pixel data. */ private void projectStackSum(ProjectionContext ctx, int stepping, int start, int end, boolean doMinMax) { projectStackMeanOrSum(ctx, stepping, start, end, false, doMinMax); } /** * Projects a stack based on the sum intensity at each XY coordinate with * the option to also average the sum intensity. * @param ctx The context of our projection. * @param pixelsType The destination Pixels type. If <code>null</code>, the * source Pixels set pixels type will be used. * @param stepping Stepping value to use while calculating the projection. * For example, <code>stepping=1</code> will use every optical section from * <code>start</code> to <code>end</code> where <code>stepping=2</code> will * use every other section from <code>start</code> to <code>end</code> to * perform the projection. * @param start Optical section to start projecting from. * @param end Optical section to finish projecting. * @param mean Whether or not we're performing an average post sum * intensity projection. * @param doMinMax Whether or not to calculate the minimum and maximum of * the projected pixel data. */ private void projectStackMeanOrSum(ProjectionContext ctx, int stepping, int start, int end, boolean mean, boolean doMinMax) { double planeMaximum = ctx.to.getMaximum(); int currentPlaneStart; double projectedValue, stackValue; double minimum = ctx.minimum; double maximum = ctx.maximum; for (int i = 0; i < ctx.planeSizeInPixels; i++) { projectedValue = 0; int projectedPlaneCount = 0; for (int z = start; z < end; z += stepping) { currentPlaneStart = ctx.planeSizeInPixels * z; stackValue = ctx.from.getPixelValue(currentPlaneStart + i); projectedValue += stackValue; projectedPlaneCount++; } if (mean) { projectedValue = projectedValue / projectedPlaneCount; } if (projectedValue > planeMaximum) { projectedValue = planeMaximum; } ctx.to.setPixelValue(i, projectedValue); if (doMinMax) { minimum = projectedValue < minimum? projectedValue : minimum; maximum = projectedValue > maximum? projectedValue : maximum; } } ctx.minimum = minimum; ctx.maximum = maximum; } /** * Stores the context of a projection operation. * * Class is static to prevent any instances from holding onto * {@link ProjectionBean} instances. * * @author Chris Allan      <a * href="mailto:callan@blackcat.ca">callan@blackcat.ca</a> * @since OMERO-Beta3.1 */ private static class ProjectionContext { /** The Pixels set we're currently working on. */ public Pixels pixels; /** Count of the number of pixels per plane for <code>pixels</code>. */ public int planeSizeInPixels; /** Current minimum for the projected pixel data. */ public double minimum = Double.MAX_VALUE; /** Current maximum for the projected pixel data. */ public double maximum = Double.MIN_VALUE; /** The raw pixel data from the stack to project from. */ public PixelData from; /** The raw pixel data buffer to project into. */ public PixelData to; } }