
com.actelion.research.orbit.imageprovider.OrbitImageBioformatsOmero Maven / Gradle / Ivy
/*
* Orbit, a versatile image analysis software for biological image-based quantification.
* Copyright (C) 2009 - 2017 Actelion Pharmaceuticals Ltd., Gewerbestrasse 16, CH-4123 Allschwil, Switzerland.
*
* This program 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.
*
* 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, see .
*
*/
package com.actelion.research.orbit.imageprovider;
import com.actelion.research.orbit.dal.IOrbitImageMultiChannel;
import com.actelion.research.orbit.exceptions.OrbitImageServletException;
import com.actelion.research.orbit.utils.ChannelToHue;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import loci.common.services.ServiceFactory;
import loci.formats.ChannelMerger;
import loci.formats.FormatException;
import loci.formats.IFormatReader;
import loci.formats.MinMaxCalculator;
import loci.formats.gui.AWTImageTools;
import loci.formats.gui.BufferedImageReader;
import loci.formats.meta.IMetadata;
import loci.formats.services.OMEXMLService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.media.jai.PlanarImage;
import java.awt.*;
import java.awt.image.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
public class OrbitImageBioformatsOmero implements IOrbitImageMultiChannel {
private static final Logger logger = LoggerFactory.getLogger(OrbitImageBioformatsOmero.class);
public static final Cache tileCache = CacheBuilder.
newBuilder().
maximumSize(40). // 40 tiles - should be enough to fill a screen
expireAfterWrite(5, TimeUnit.MINUTES).
build();
public static final int TILE_SIZE_DEFAULT = 512;
final int maxThumbWidth = 300;
final protected ThreadLocal reader;
final List allReaders = Collections.synchronizedList(new ArrayList());
private String filename;
private String originalFilename;
private int optimalTileWidth;
private int optimalTileHeight;
private int numLevels;
private int level;
private boolean interleaved;
private ColorModel colorModel;
private SampleModel sampleModel;
private int numBands;
private int numBandsOriginal;
private int originalBitsPerSample;
private boolean originalWasGrayScale;
private int tileGridXOffset;
private int tileGridYOffset;
private int minX;
private int minY;
private long width;
private long height;
private boolean is16bit = false;
protected static final Map minMaxCache = new ConcurrentHashMap<>();
private String[] channelNames;
protected static final ColorModel rgbColorModel = new BufferedImage(1,1,BufferedImage.TYPE_INT_RGB).getColorModel();
private int series = 0;
private float[] channelContributions = null;
private float[] hueMap;
private boolean useCache;
private ImageProviderOmero.GatewayAndCtx gatewayAndCtx;
private long imageId;
private long groupId;
public OrbitImageBioformatsOmero(final String filename, final int level, final int series, boolean useCache, ImageProviderOmero.GatewayAndCtx gatewayAndCtx, final long imageId, long groupId) throws IOException, FormatException {
this.originalFilename = filename;
this.series = series;
this.filename = filename+"["+level+"]"+" ["+ series +"]"; // level/series here is important because filename is part of key for hashing!
this.level = level;
this.useCache = useCache;
this.gatewayAndCtx = gatewayAndCtx;
this.imageId = imageId;
this.groupId = groupId;
logger.info("bioformats image: "+this.filename);
reader = new ThreadLocal() {
@Override
protected BufferedImageReader initialValue() {
try {
logger.debug("init bioformats: "+filename+" ["+level+"]"+" ["+ series +"]");
IFormatReader r = getIFormatReader(filename, level);
ServiceFactory factory = new ServiceFactory();
OMEXMLService service = factory.getInstance(OMEXMLService.class);
IMetadata meta = service.createOMEXMLMetadata();
r.setMetadataStore(meta);
r.setId("omeroorbit:iid="+imageId);
if (series>=r.getSeriesCount()) {
close();
throw new OrbitImageServletException("image series " + series + " does not exist for image " + filename+" Max series count: "+r.getSeriesCount());
}
//setReaderSeries(r, filename);
r.setSeries(series);
r.setResolution(level);
//if (mergeRGBChannels(r.isRGB(), r.getSizeC(), r.getRGBChannelCount() ,meta))
if (((OmeroReaderOrbit)r).isRGBImage())
{
r = new ChannelMerger(r);
}
BufferedImageReader bir;
if (doMergeChannels(r)) {
// fluo images
if (channelNames == null) {
channelNames = new String[r.getSizeC()];
for (int c = 0; c < r.getSizeC(); c++) {
String name = meta.getChannelName(r.getSeries(), c);
if (name == null) {
name = "Channel" + c;
}
logger.info("channel name " + c + ": " + name);
channelNames[c] = name;
}
}
// build hueMap
hueMap = getHues();
}
is16bit = r.getBitsPerPixel()>8;
logger.debug("is16bit: "+is16bit);
synchronized (minMaxCache) {
final FilenameSeries key = new FilenameSeries(originalFilename,series);
if (is16bit && !minMaxCache.containsKey((key)))
{
IFormatReader r2 = getIFormatReader(filename, r.getResolutionCount() - 1);
r2.setId("omeroorbit:iid="+imageId);
r2.setSeries(series);
MinMaxCalculator minMax = new MinMaxCalculator(r2);
int[] nos = minMax.getZCTCoords(0);
int z = nos[0], t = nos[2];
for (int ic=0; ic= numLevels) {
close();
throw new OrbitImageServletException("image pyramid level " + this.level + " does not exist for image " + filename);
}
width = reader.get().getSizeX();
height = reader.get().getSizeY();
numBandsOriginal = reader.get().getSizeC();
BufferedImageReader bir = reader.get();
if (bir.getResolution()!=this.level) bir.setResolution(this.level);
// optimalTileWidth = bir.getOptimalTileWidth();
// optimalTileHeight = bir.getOptimalTileHeight();
// if (optimalTileWidth< TILE_SIZE_DEFAULT && width>=TILE_SIZE_DEFAULT) optimalTileWidth = TILE_SIZE_DEFAULT;
// if (optimalTileHeight< TILE_SIZE_DEFAULT && height>=TILE_SIZE_DEFAULT) optimalTileHeight = TILE_SIZE_DEFAULT;
// bir.getOptimalTileWidth() seems to return the full with of the image, thus we use a fixed tilesize here
optimalTileWidth = TILE_SIZE_DEFAULT;
optimalTileHeight = TILE_SIZE_DEFAULT;
logger.debug("tile size: "+optimalTileWidth+" x "+optimalTileHeight);
originalBitsPerSample = bir.getBitsPerPixel();
interleaved = bir.isInterleaved();
try {
BufferedImage img = getPlane(0, 0, null);
colorModel = img.getColorModel();
sampleModel = img.getSampleModel();
numBands = sampleModel.getNumBands();
originalWasGrayScale = numBands==1;
tileGridXOffset = img.getTileGridXOffset();
tileGridYOffset = img.getTileGridYOffset();
minX = img.getMinX();
minY = img.getMinY();
} catch (Exception e) {
if (this.level 1 && !r.isRGB();
}
@Override
public String readInfoString(String filename) throws OrbitImageServletException {
return "";
}
@Override
public Raster getTileData(int tileX, int tileY) {
return getTileData(tileX, tileY, null);
}
@Override
public BufferedImage getOverviewImage() {
return null;
}
@Override
public Raster getTileData(int tileX, int tileY, float[] channelContributions) {
try {
BufferedImage img = getPlane(tileX, tileY, channelContributions!=null?channelContributions:this.channelContributions);
// ensure tiles have always full tileWidth and tileHeight (even at borders)
if (img.getWidth()!=getTileWidth() || img.getHeight()!=getTileHeight())
{
BufferedImage bi = new BufferedImage(getTileWidth(), getTileHeight(), img.getType());
bi.getGraphics().drawImage(img, 0, 0, null);
img = bi;
}
// set correct bounds
Raster r = img.getData().createTranslatedChild(PlanarImage.tileXToX(tileX, img.getTileGridXOffset(), getTileWidth()), PlanarImage.tileYToY(tileY, img.getTileGridYOffset(), getTileHeight()));
return r;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
protected BufferedImage getPlane(int tileX, int tileY, final float[] channelContributions) throws Exception {
int x = optimalTileWidth * tileX;
int y = optimalTileHeight * tileY;
int w = (int) Math.min(optimalTileWidth, width - x);
int h = (int) Math.min(optimalTileHeight, height - y);
final FilenameSeries key = new FilenameSeries(originalFilename,series);
BufferedImageReader bir = reader.get();
if (bir.getResolution()!=this.level) bir.setResolution(this.level);
if (!doMergeChannels(bir)) { // brightfield or just one grayscale channel
BufferedImage bi = bir.openImage(0,x,y,w,h);
if (bi!=null && bi.getType()==BufferedImage.TYPE_INT_RGB) return bi;
else {
if (is16bit) {
int minIntens = minMaxCache.get(key).getMin()[0];
int maxIntens = minMaxCache.get(key).getMax()[0];
bi = AWTImageTools.autoscale(bi,minIntens,maxIntens);
}
BufferedImage biRGB = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
biRGB.getGraphics().drawImage(bi, 0, 0, null);
return biRGB;
}
}
else { // fluo -> merge channels
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_RGB);
WritableRaster raster = bi.getRaster();
int no = 0;
int sizeC = reader.get().getSizeC();
int[] nos = reader.get().getZCTCoords(no);
int z = nos[0], t = nos[2];
int col;
int[] pix = new int[3];
for (int c = 0; c < sizeC; c++) {
if (isChannelActive(c)) {
int index = reader.get().getIndex(z, c, t);
ROIDef roiDef = new ROIDef(filename,level, index,x,y,w,h);
BufferedImage bit = useCache? OrbitImageBioformatsOmero.tileCache.getIfPresent(roiDef): null;
if (bit==null) {
bit = reader.get().openImage(index, x, y, w, h);
if (useCache) OrbitImageBioformatsOmero.tileCache.put(roiDef,bit);
}
//bit = AWTImageTools.autoscale(bit,minMaxCache.get(key).getMin()[c] , minMaxCache.get(key).getMax()[c]);
Object pixels = AWTImageTools.getPixels(bit, 0, 0, 1, 1);
int minIntens = 0;
int maxIntens = 256;
if (is16bit) {
minIntens = minMaxCache.get(key).getMin()[c];
maxIntens = minMaxCache.get(key).getMax()[c];
}
for (int iy = 0; iy < h; iy++) {
for (int ix = 0; ix < w; ix++) {
int s = 0;
for (int b = 0; b < bit.getSampleModel().getNumBands(); b++) {
int intens = bit.getRaster().getSample(ix, iy, b);
if (is16bit) {
intens = autoscale(intens, pixels, minIntens, maxIntens);
}
s += intens;
}
if (channelContributions!=null) {
s *= channelContributions[c];
}
int intens = s <= 255 ? s : 255;
pix = raster.getPixel(ix, iy, pix);
col = Color.HSBtoRGB(hueMap[c], 1f, intens / 255f);
pix[0] += (col >> 16) & 0xFF; // red
pix[1] += (col >> 8) & 0xFF; // green
pix[2] += col & 0xFF; // blue
if (pix[0] > 255) pix[0] = 255;
if (pix[1] > 255) pix[1] = 255;
if (pix[2] > 255) pix[2] = 255;
raster.setPixel(ix, iy, pix);
}
}
} // channelActive?
} // channels
return bi;
} // fluo
}
/**
* see AWTImageTools.autoscale()
*/
private int autoscale(int s, Object pixels, int min, int max) {
int out;
if (pixels instanceof byte[][]) return s;
else if (pixels instanceof short[][]) {
if (s < 0) s += 32767;
int diff = max - min;
float dist = (float) (s - min) / diff;
if (s >= max) out = 255;
else if (s <= min) out = 0;
else out = (int) (dist * 256);
return out;
} else if (pixels instanceof int[][]) {
if (s >= max) out = 255;
else if (s <= min) out = 0;
else {
int diff = max - min;
float dist = (s - min) / diff;
out = (int) (dist * 256);
}
return out;
} else if (pixels instanceof float[][]) {
if (s >= max) out = 255;
else if (s <= min) out = 0;
else {
int diff = max - min;
float dist = (s - min) / diff;
out = (int) (dist * 256);
}
return out;
} else if (pixels instanceof double[][]) {
if (s >= max) out = 255;
else if (s <= min) out = 0;
else {
int diff = max - min;
float dist = (float) (s - min) / diff;
out = (int) (dist * 256);
}
return out;
}
return s;
}
@Override
public String getFilename() {
return filename;
}
@Override
public int getWidth() {
return (int) width;
}
@Override
public int getHeight() {
return (int) height;
}
@Override
public int getTileWidth() {
return optimalTileWidth;
}
@Override
public int getTileHeight() {
return optimalTileHeight;
}
@Override
public int getTileGridXOffset() {
return tileGridXOffset;
}
@Override
public int getTileGridYOffset() {
return tileGridYOffset;
}
@Override
public int getMinX() {
return minX;
}
@Override
public int getMinY() {
return minY;
}
@Override
public int getNumBands() {
return numBands;
}
@Override
public ColorModel getColorModel() {
return rgbColorModel;
}
@Override
public SampleModel getSampleModel() {
return rgbColorModel.createCompatibleSampleModel(optimalTileWidth,optimalTileHeight);
}
@Override
public int getOriginalBitsPerSample() {
return originalBitsPerSample;
}
@Override
public boolean getOriginalWasGrayScale() {
return originalWasGrayScale;
}
@Override
public void close() throws IOException {
synchronized (allReaders) {
for (IFormatReader r: allReaders) {
try {
r.close();
} catch (Exception e) {
}
}
}
}
public int getNumLevels() {
return numLevels;
}
public int getLevel() {
return level;
}
public BufferedImage getThumbnail() {
long thumbW=1;
long thumbH=1;
try {
IFormatReader ir = null;
for (int lev=numLevels-1; lev>=0; lev--) {
if (ir!=null) ir.close();
ir = getIFormatReader(reader.get().getCurrentFile(),lev);
ir.setId(reader.get().getCurrentFile());
ir.setSeries(reader.get().getSeries());
ir.setResolution(lev);
thumbW = ir.getSizeX();
thumbH = ir.getSizeY();
double diff = Math.abs((thumbW/(double)thumbH) - (width/(double)height));
logger.trace("thumb lev: "+lev+" diff: "+diff+" WxH: "+thumbW+"x"+thumbH);
if (diff<0.001) break;
}
BufferedImageReader bir = BufferedImageReader.makeBufferedImageReader(ir);
BufferedImage thumb = bir.openImage(0);
bir.close();
if (thumbW>maxThumbWidth) {
int h = (int)(maxThumbWidth * (thumbH/(double)thumbW));
BufferedImage img = new BufferedImage(maxThumbWidth,h, BufferedImage.TYPE_INT_RGB);
Graphics2D g2d = (Graphics2D) img.getGraphics();
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
g2d.drawImage(thumb,0,0,maxThumbWidth,h, null);
thumb = img;
}
return thumb;
} catch (IOException e) {
e.printStackTrace();
} catch (FormatException e) {
e.printStackTrace();
}
return null;
}
@Override
public String[] getChannelNames() {
return channelNames;
}
@Override
public void setChannelNames(String[] channelNames) {
this.channelNames = channelNames;
}
@Override
public float[] getChannelContributions() {
return channelContributions;
}
@Override
public void setChannelContributions(float[] contributions) {
if (contributions==null) {
channelContributions = null;
return;
}
if (channelContributions==null) {
channelContributions = new float[contributions.length];
}
System.arraycopy(contributions,0,channelContributions, 0, contributions.length);
}
@Override
public float[] getHues() {
if (channelNames==null) return null;
float[] hues = new float[channelNames.length];
for (int c=0; c0.00001f;
}
public class FilenameSeries {
String filename;
int series;
public FilenameSeries(String filename, int series) {
this.filename = filename;
this.series = series;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
public int getSeries() {
return series;
}
public void setSeries(int series) {
this.series = series;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
FilenameSeries that = (FilenameSeries) o;
if (series != that.series) return false;
return filename != null ? filename.equals(that.filename) : that.filename == null;
}
@Override
public int hashCode() {
int result = filename != null ? filename.hashCode() : 0;
result = 31 * result + series;
return result;
}
}
public static class MinMaxPerChan {
private int[] min;
private int[] max;
public MinMaxPerChan(int[] min, int[] max) {
this.min = min;
this.max = max;
}
public int[] getMin() {
return min;
}
public void setMin(int[] min) {
this.min = min;
}
public int[] getMax() {
return max;
}
public void setMax(int[] max) {
this.max = max;
}
}
public int getSeries() {
return series;
}
private class ROIDef {
String filename;
int level,index,x,y,w,h;
public ROIDef(String filename, int level, int index, int x, int y, int w, int h) {
this.filename = filename;
this.level = level;
this.index = index;
this.x = x;
this.y = y;
this.w = w;
this.h = h;
}
@Override
public String toString() {
return "ROIDef{" +
"filename='" + filename + '\'' +
", level=" + level +
", index=" + index +
", x=" + x +
", y=" + y +
", w=" + w +
", h=" + h +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ROIDef roiDef = (ROIDef) o;
if (level != roiDef.level) return false;
if (index != roiDef.index) return false;
if (x != roiDef.x) return false;
if (y != roiDef.y) return false;
if (w != roiDef.w) return false;
if (h != roiDef.h) return false;
return filename != null ? filename.equals(roiDef.filename) : roiDef.filename == null;
}
@Override
public int hashCode() {
int result = filename != null ? filename.hashCode() : 0;
result = 31 * result + level;
result = 31 * result + index;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + w;
result = 31 * result + h;
return result;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy