/*========================================================================= Program: FusionViewer Module: $RCSfile: ImageTextureRenderer.java,v $ Language: Java Date: $Date: 2008/01/11 21:37:30 $ Version: $Revision: 1.7 $ Copyright (c) Insightful Corporation. All rights reserved. See Copyright.txt for details. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notice for more information. =========================================================================*/ package org.fusionviewer.display; import java.nio.ByteBuffer; import java.nio.ByteOrder; import org.fusionviewer.io.NativeBuffer; import org.fusionviewer.model.ImageDisplayModel; import javax.media.opengl.GL; import javax.media.opengl.GLAutoDrawable; /**\class ImageTextureRenderer *\brief Renders an image into an OpenGL context. * * There are 2 possible rendering paths. The first renders the * image to a texture and draws a texture-mapped quad. * The second uses glDrawPixels to copy the buffer. */ public class ImageTextureRenderer { private final static int DIMENSION = 3; private Image m_image; // image to draw private ImageDisplayModel m_viewModel; // view parameters private int[] m_imageSize = new int[DIMENSION]; // image size rearranged for the current orientation private float[] m_pixelSpacing = new float[DIMENSION]; // pixel spacing rearranged for the current orientation private SliceConfiguration m_sliceConfig; // mapping from image axes to display axes private ImageSliceView m_view; // view to draw into private ByteBuffer m_textureBuffer; // image formatted for transfer to texture memory private NativeBuffer m_sliceBuffer; // cached slice of the image converted to 12-bit private int m_texturePitch, m_textureHeight; // texture dimensions private int[] m_textureName = new int[1]; // texture name for binding private int m_cachedSlice = -1; // cached slice in m_sliceBuffer private boolean m_directToTexture = true; // true if we are bypassing m_sliceBuffer // GL contexts can only be accessed within GLEventListener callbacks. We therefore need // to set flags to control various code paths through the drawImage method (called from // m_view's display method). private boolean m_newTexture; // true if we need to allocate the GL texture private boolean m_disposingTextures = false; // true when textures should be disposed /** * Create a new image renderer. * * @param view view to draw into */ public ImageTextureRenderer(ImageSliceView view) { m_view = view; } /** * Set the image this view should render. * * @param sliceConfig orientation of the 2D slice to draw * @param image image to draw * @param model image parameters */ public void setImage(SliceConfiguration sliceConfig, Image image, ImageDisplayModel model) { if (model == null) { release(); return; } m_sliceConfig = sliceConfig; m_image = image; m_viewModel = model; for (int i = 0; i < DIMENSION; i++) { int axisIndex = sliceConfig.getAxisIndex(i); m_imageSize[i] = image.getDimension(axisIndex); m_pixelSpacing[i] = image.getPixelSpacing(axisIndex); } m_newTexture = true; } /** * Dispose of resources used by this view. */ public void release() { m_textureBuffer = null; if (m_sliceBuffer != null) m_sliceBuffer.release(); m_sliceBuffer = null; disposeTexture(); m_texturePitch = m_textureHeight = 0; m_newTexture = true; m_cachedSlice = -1; m_directToTexture = true; } /* * Initialize memory for the image. */ private void initTexture(GLAutoDrawable drawable) { GL gl = drawable.getGL(); if (m_textureName[0] != 0) { gl.glDeleteTextures(1, m_textureName, 0); m_textureName[0] = 0; } if (m_sliceBuffer != null) { m_sliceBuffer.release(); m_sliceBuffer = null; } gl.glPixelStorei(GL.GL_UNPACK_ALIGNMENT, 1); gl.glGenTextures(1, m_textureName, 0); gl.glBindTexture(GL.GL_TEXTURE_2D, m_textureName[0]); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_S, GL.GL_CLAMP); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_WRAP_T, GL.GL_CLAMP); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MAG_FILTER, GL.GL_LINEAR); gl.glTexParameteri(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_MIN_FILTER, GL.GL_LINEAR); gl.glTexEnvi(GL.GL_TEXTURE_ENV, GL.GL_TEXTURE_ENV_MODE, GL.GL_REPLACE); gl.glTexParameterfv(GL.GL_TEXTURE_2D, GL.GL_TEXTURE_BORDER_COLOR, new float[] {0.0f, 0.0f, 0.0f, 0.0f}, 0); int imageWidth = m_imageSize[0]; int imageHeight = m_imageSize[1]; // For large images, window and level is much slower in texture mode but zooming and // panning is much faster. The alternate rendering mode puts both tasks at a speed // between the 2 extremes, so we go to this mode for 2D images. if (!allocateTexture(gl, nextPower2(imageWidth), nextPower2(imageHeight))) { // If the image cannot fit as a texture, draw it directly from memory gl.glDeleteTextures(1, m_textureName, 0); m_textureName[0] = 0; m_directToTexture = false; // Allocate a slice buffer the same dimensions as the image and 2 bytes per pixel m_sliceBuffer = new NativeBuffer(imageWidth * imageHeight * 2); allocateTextureForIndirect(); } m_cachedSlice = -1; } /* * Allocate memory needed to manage the texture for this image. */ private boolean allocateTexture(GL gl, int width, int height) { // Test to see if the specified texture size will work gl.glTexImage2D(GL.GL_PROXY_TEXTURE_2D, 0, 4, width, height, 0, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, (ByteBuffer) null); int[] textureWidth = new int[1]; gl.glGetTexLevelParameteriv(GL.GL_PROXY_TEXTURE_2D, 0, GL.GL_TEXTURE_WIDTH, textureWidth, 0); if (textureWidth[0] != 0) { m_textureBuffer = ByteBuffer.allocateDirect(m_imageSize[0] * m_imageSize[1] * 4); m_textureBuffer.order(ByteOrder.nativeOrder()); //buffer rewind required by JSR 231 m_textureBuffer.rewind(); gl.glTexImage2D(GL.GL_TEXTURE_2D, 0, 4, width, height, 0, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, (ByteBuffer) null); m_texturePitch = width * 4; m_textureHeight = height; return true; } return false; } /* * If texture memory cannot be used (e.g., image is too large), allocate a temporary buffer. */ private void allocateTextureForIndirect() { // Allocate a buffer twice the size of the window. This will allow the window to expand // without needing to allocate a new buffer right away. m_textureBuffer = ByteBuffer.allocateDirect((m_view.getParent().getWidth() * 4 * m_view.getParent().getHeight()) * 2); m_textureBuffer.order(ByteOrder.nativeOrder()); } /* * Dispose of any textures in use. */ private void disposeTexture() { if (m_textureName[0] == 0) return; // We cannot reliably get a valid GL context from GLCanvas, // when we are not executing a GLEventListener callback. // To run any commands on a GL context, we need to set a flag // and trigger a draw event. m_disposingTextures = true; m_view.getParent().display(); m_disposingTextures = false; } /* * Returns x if it is a power of 2. Otherwise, return the next integer greater * than x that is a power of 2. */ private int nextPower2(int x) { return (int) Math.pow(2, Math.ceil(Math.log(x) / Math.log(2))); } /** * Draw the image. */ public void drawImage(GLAutoDrawable drawable, int originX, int originY, int imageId, boolean reload) { GL gl = drawable.getGL(); // We need a GL context to delete textures, so the following flag is used // in conjunction with the disposeTextures method. if (m_disposingTextures) { gl.glDeleteTextures(1, m_textureName, 0); m_textureName[0] = 0; return; } if (m_newTexture) { initTexture(drawable); m_newTexture = false; reload = true; } int slice = 0; //get reference image size at Z-Axis int zAxis = m_sliceConfig.getAxisIndex(2); float zSize = m_pixelSpacing[2] * m_imageSize[2]; float refZSize = m_view.getReferenceSize(zAxis); if (refZSize - zAxis >= 0.01 ) { float sizeRatio = zSize / refZSize; slice = (int) (m_viewModel.getPosition(imageId, zAxis) / m_pixelSpacing[2] * sizeRatio); } else{ slice = (int) (m_viewModel.getPosition(imageId, zAxis) / m_pixelSpacing[2]); } if (loadSlice(slice)) reload = true; if (m_directToTexture) { drawTexture(drawable, imageId, originX, originY, reload); reload = false; } else { drawScaledImage(drawable, imageId, originX, originY); } } /* * Load image slice into temporary memory if we are not rendering to a texture. */ private boolean loadSlice(int slice) { if (slice == m_cachedSlice) return false; m_cachedSlice = slice; if (!m_directToTexture) m_image.loadSlice(m_sliceConfig.getAxisIndex(0), m_sliceConfig.getAxisIndex(1), slice, m_sliceBuffer.getPointer()); return true; } /* * Load the image slice into a texture and draw it mapped onto a GL quad */ private void drawTexture(GLAutoDrawable drawable, int imageId, int originX, int originY, boolean reloadTexture) { GL gl = drawable.getGL(); gl.glBindTexture(GL.GL_TEXTURE_2D, m_textureName[0]); int imageWidth = m_imageSize[0]; int imageHeight = m_imageSize[1]; boolean flipImage = m_sliceConfig.getAxisFlipped(1); if (reloadTexture) { m_image.loadSliceWindowLevel(m_sliceConfig.getAxisIndex(0), m_sliceConfig.getAxisIndex(1), m_cachedSlice, 0, 0, m_imageSize[0], m_imageSize[1], m_viewModel.getOffset(imageId), m_viewModel.getSlope(imageId), m_viewModel.getColormap(imageId).getColormap(), m_viewModel.isInverted(imageId), m_textureBuffer); gl.glTexSubImage2D(GL.GL_TEXTURE_2D, 0, 0, 0, imageWidth, imageHeight, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, m_textureBuffer); } gl.glPushMatrix(); gl.glTranslatef(originX - m_view.getXOrigin() + 0.375f, originY - m_view.getYOrigin() + 0.375f, 0.0f); float texX = (float) imageWidth / (float) (m_texturePitch / 4); float texY = (float) imageHeight / (float) m_textureHeight; float scaleX = m_pixelSpacing[0] / m_view.getPixelSpacing(0); float scaleY = m_pixelSpacing[1] / m_view.getPixelSpacing(1); // Manually scale to eliminate potential inconsistency in rounding by // the OpenGL driver. Clamp the boundaries to inside the view port. imageWidth = (int) (imageWidth * scaleX + 0.5f); imageHeight = (int) (imageHeight * scaleY + 0.5f); if (flipImage) { gl.glTranslatef(0.0f, imageHeight, 0.0f); gl.glScalef(1.0f, -1.0f, 1.0f); } gl.glColor4f(1.0f, 1.0f, 1.0f, 1.0f); gl.glEnable(GL.GL_TEXTURE_2D); gl.glBegin(GL.GL_QUADS); gl.glTexCoord2f ( 0.0f, 0.0f ); gl.glVertex2i ( 0, imageHeight ); gl.glTexCoord2f ( texX, 0.0f ); gl.glVertex2i(imageWidth, imageHeight); gl.glTexCoord2f ( texX, texY ); gl.glVertex2i (imageWidth, 0); gl.glTexCoord2f ( 0.0f, texY ); gl.glVertex2i ( 0, 0 ); gl.glEnd(); gl.glDisable(GL.GL_TEXTURE_2D); gl.glPopMatrix(); } /* * Scale the image slice into a memory buffer and draw it */ private void drawScaledImage(GLAutoDrawable drawable, int imageId, int originX, int originY) { GL gl = drawable.getGL(); // Convert window and level specified in the image range of values // to the 0-4095 range float slope; int offset; boolean flipImage = m_sliceConfig.getAxisFlipped(1); if (m_image.isByteType()) { slope = m_viewModel.getSlope(imageId); offset = (int) m_viewModel.getOffset(imageId); } else { double range = m_image.getMaxValue() - m_image.getMinValue(); double window = m_viewModel.getWindow(imageId) / range * 4095.0; double level = (m_viewModel.getLevel(imageId) - m_image.getMinValue()) / range * 4095.0; slope = (float) (255.0 / window); offset = (int) (level - (window / 2.0)); } float scaleX = m_pixelSpacing[0] / m_view.getPixelSpacing(0); float scaleY = m_pixelSpacing[1] / m_view.getPixelSpacing(1); int imageWidth = m_imageSize[0]; int imageHeight = m_imageSize[1]; int viewWidth = m_view.getViewWidth(); int viewHeight = m_view.getViewHeight(); int scaledWidth = (int) (imageWidth * scaleX); int scaledHeight = (int) (imageHeight * scaleY); int outputWidth = (scaledWidth > viewWidth ? viewWidth : scaledWidth); int outputHeight = (scaledHeight > viewHeight ? viewHeight : scaledHeight); if (outputWidth * 4 * outputHeight > m_textureBuffer.capacity()) allocateTextureForIndirect(); Colormap colormap = m_viewModel.getColormap(imageId); m_image.scaleToOutput(m_sliceBuffer.getPointer(), m_textureBuffer, imageWidth, imageHeight, outputWidth, outputHeight, m_view.getXOrigin(), m_view.getYOrigin(), scaleX, scaleY, offset, slope, colormap.getColormap(), m_viewModel.isInverted(imageId), !flipImage, !colormap.isRamp()); gl.glRasterPos2i(originX, originY); gl.glDrawPixels(outputWidth, outputHeight, GL.GL_BGRA, GL.GL_UNSIGNED_BYTE, m_textureBuffer); } }