/*============================================================================= Project: SharpImage Module: siOpenGlSliceRenderer.cs Language: C# Author: Dan Mueller Date: $Date: 2007-08-25 10:48:24 +1000 (Sat, 25 Aug 2007) $ Revision: $Revision: 17 $ Copyright (c) Queensland University of Technology (QUT) 2007. All rights reserved. This software is distributed WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the above copyright notices for more information. =============================================================================*/ using System; using System.IO; using System.Collections.Generic; using System.Text; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Windows.Forms; using System.Diagnostics; using System.Runtime.InteropServices; using Tao.OpenGl; using Tao.FreeGlut; using Tao.Platform.Windows; using SharpImage.Main; namespace SharpImage.Rendering { #region siOpenGlSliceRenderer Class //========================================================================= /// /// Renders 2-D and 3-D images as slices along the z-axis using the OpenGL /// library. Multiple inputs are rendered from Input[0]and up /// (eg. Input[1] will overwrite Input[0] if it contains no /// transparency). The input image assumes the default lookup table unless /// the image is tagged with the Metadata key "LookupTable{0}". /// public class siOpenGlSliceRenderer : siRenderer, IDisposable { #region Constants //===================================================================== protected static readonly Color s_BackColor = Color.White; protected const int SPACE_AROUND_IMAGE = 2; protected const double MINIMUM_SCALE_FACTOR = 0.25; //===================================================================== #endregion #region Instance Variables //===================================================================== private bool m_HasRenderedFirstTime = false; private bool m_IsDisposed = false; private int m_InputAsSliceTexId = 0; private ToolTip m_ToolTipSlice; private VScrollBar m_SliceSlider; private List m_ExtractFilters; //===================================================================== #endregion #region Construction and Disposal //===================================================================== /// /// Public constructor. /// public siOpenGlSliceRenderer(IApplication parent) : base("OpenGl Viewer", parent, new siOpenGlRendererForm()) { // Setup Metadata this.BackColor = s_BackColor; this.Metadata["IsTranslatingImage"] = false; this.Metadata["IsMouseInsideImageSpace"] = false; this.Metadata["ZoomFactor"] = (double)1.0; this.Metadata["CurrentSlice"] = (int)0; // Initialise extract filters this.m_ExtractFilters = new List(); // Initialise slider m_SliceSlider = new VScrollBar(); m_SliceSlider.Dock = DockStyle.Right; m_SliceSlider.MouseEnter += new EventHandler(m_SliceSlider_MouseEvent); m_SliceSlider.MouseHover += new EventHandler(m_SliceSlider_MouseEvent); m_SliceSlider.Scroll += new ScrollEventHandler(m_SliceSlider_Scroll); this.Form.Controls.Add(this.m_SliceSlider); // Initialise tooltip this.m_ToolTipSlice = new ToolTip(); this.m_ToolTipSlice.UseFading = false; // Add event handlers this.SliceChanged += new SliceChangedHandler(OpenGlSliceRenderer_SliceChanged); } /// /// Deconstructor. /// ~siOpenGlSliceRenderer() { // Call Dispose with false. Since we're in the // destructor call, the managed resources will be // disposed of anyways. this.Dispose(false); } /// /// Dispose of any resources. /// public override void Dispose() { // Dispose both managed and unmanaged resources this.Dispose(true); // Tell the GC that the Finalize process no longer needs // to be run for this object. GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposeManagedResources) { // Check that we are not already disposed if (this.m_IsDisposed) return; // Only get here if not already disposed if (disposeManagedResources) { if (this.InputsAsSlice != null) { this.InputsAsSlice.Clear(); this.m_InputsAsSlice = null; } if (this.m_ExtractFilters != null) { foreach (itk.itkExtractImageFilter filter in this.m_ExtractFilters) { filter.Dispose(); } this.m_ExtractFilters.Clear(); this.m_ExtractFilters = null; } if (this.m_SliceSlider != null) { this.m_SliceSlider.Dispose(); this.m_SliceSlider = null; } } // Dispose base base.Dispose(); this.m_IsDisposed = true; } //===================================================================== #endregion #region Properties //===================================================================== #region ZoomFactor //===================================================================== /// /// Gets/sets the zoom factor for viewing the image. /// A zoom factor of 1.0 means we render 1:1. /// A zoom factor of 2.0 means we render 2:1 (zoomed in). /// A zoom factor of 0.5 means we render 1:2 (zoomed out). /// protected double ZoomFactor { get { return (double)this.Metadata["ZoomFactor"]; } set { this.Metadata["ZoomFactor"] = value; } } //===================================================================== #endregion #region Offset //===================================================================== /// /// Gets/sets the top-left image offset. /// This property controls image translation. /// protected PointF Offset { get { if (!this.ContainsMetadata("ImageScreenOffset")) this.Offset = new PointF(0.0F, 0.0F); return (PointF)this.Metadata["ImageScreenOffset"]; } set { this.Metadata["ImageScreenOffset"] = value; } } //===================================================================== #endregion #region InputsAsSlice //===================================================================== private List m_InputsAsSlice = new List(); /// /// Gets the list of Input objects as image slices. /// public List InputsAsSlice { get { return this.m_InputsAsSlice; } } //===================================================================== #endregion //===================================================================== #endregion #region Events //===================================================================== #region SliceChanged //===================================================================== /// /// EventArg delegate for allowing listeners access to the /// RendererBase which rasied the event. /// /// public delegate void SliceChangedHandler(siOpenGlSliceRenderer renderer, siSliceChangedEventArgs e); private SliceChangedHandler m_StorageSliceChanged; /// /// An event raised when the current slice has been changed. /// public event SliceChangedHandler SliceChanged { add { this.m_StorageSliceChanged += value; } remove { this.m_StorageSliceChanged -= value; } } /// /// Fire the SliceChanged event. /// protected void FireSliceChanged(siSliceChangedEventArgs e) { // Fire event if (this.m_StorageSliceChanged != null) this.m_StorageSliceChanged(this, e); } //===================================================================== #endregion //===================================================================== #endregion #region Public Methods //===================================================================== /// /// Initialises the GdiSliceRenderer. /// public override void Initialise() { // Call the base base.Initialise(); // Init form and inputs this.Form.Initialise(this); this.InitialiseInputs(); // Get the input and inputAsSlice const int indexInput = 0; itk.itkImageBase input = this.InputAsImage(indexInput); itk.itkImageBase inputAsSlice = this.InputsAsSlice[indexInput]; // Init OpenGL Gl.glEnable(Gl.GL_TEXTURE_2D); Gl.glEnable(Gl.GL_DEPTH_TEST); Gl.glDepthFunc(Gl.GL_LEQUAL); //// Set starting size //double[] scaledSize = new double[inputAsSlice.Dimension]; //scaledSize[0] = this.ZoomFactor * inputAsSlice.Size[0]; //for (int dim = 1; dim < inputAsSlice.Dimension; dim++) // scaledSize[dim] = this.ZoomFactor * (double)inputAsSlice.Size[dim] * (inputAsSlice.Spacing[dim] / inputAsSlice.Spacing[0]); //this.ViewportSize = new Size((int)Math.Ceiling(scaledSize[0] + 2*SPACE_AROUND_IMAGE), // (int)Math.Ceiling(scaledSize[1] + 2*SPACE_AROUND_IMAGE)); // Set slider max value this.m_SliceSlider.SmallChange = 1; this.m_SliceSlider.LargeChange = 10; this.m_SliceSlider.Minimum = 0; // Disable slider if input image has 2 dimensions if (input.Dimension == 2) this.m_SliceSlider.Enabled = false; else if (input.Dimension == 3) this.m_SliceSlider.Maximum = input.Size[2] + this.m_SliceSlider.LargeChange - 2; } /// /// Forces the Renderer to repaint. /// public override void Repaint() { base.Repaint(); this.Form.Repaint(); } /// /// Close the RendererBase and dispose of all resources. /// public override void Close() { base.Close(); this.Dispose(); } //===================================================================== #endregion #region Paint Methods //===================================================================== protected override void OnPaint(PaintEventArgs e) { // Give the base class a chance to paint base.OnPaint(e); if (!this.IsModified) return; // Get the first input for various information purposes itk.itkImageBase input = this.InputAsImage(0); // TODO: // Draw each input for (int indexInput = 0; indexInput < this.Inputs.Count; indexInput++) this.DrawInput(indexInput); // Set we have rendered this.m_HasRenderedFirstTime = true; this.IsModified = false; } private void DrawInput(int indexInput) { // Clear and draw background color Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glDrawBuffer(Gl.GL_BACK); Gl.glClear(Gl.GL_COLOR_BUFFER_BIT | Gl.GL_DEPTH_BUFFER_BIT); float[] back = siColorHelper.To4fv(this.BackColor); Gl.glClearColor(back[0], back[1], back[2], back[3]); // Change what we are looking at Gl.glMatrixMode(Gl.GL_MODELVIEW); Gl.glLoadIdentity(); //Glu.gluLookAt(this.Context.Environment_Eye[0], // this.Context.Environment_Eye[1], // this.Context.Environment_Eye[2], // this.Context.Environment_Center[0], // this.Context.Environment_Center[1], // this.Context.Environment_Center[2], // this.Context.Environment_Up[0], // this.Context.Environment_Up[1], // this.Context.Environment_Up[2]); // Set Frustum Gl.glMatrixMode(Gl.GL_PROJECTION); Gl.glLoadIdentity(); //Gl.glFrustum( // -0.5, // Left // 0.5, // Right // -0.5, // Bottom // 0.5, // Top // 1.0, // Front // 0.0 // Back // ); // Translate Gl.glMatrixMode(Gl.GL_MODELVIEW); //Gl.glTranslatef(this.Context.Transform_Translation[0], // this.Context.Transform_Translation[1], // this.Context.Transform_Translation[2]); // Rotate Gl.glMatrixMode(Gl.GL_MODELVIEW); //Gl.glMultMatrixf((float[])this.Context.Transform_Rotation); // Scale Gl.glMatrixMode(Gl.GL_MODELVIEW); //Gl.glMultMatrixf((float[])this.Context.Transform_Scale); // Draw image itk.itkImageBase inputAsSlice = this.InputsAsSlice[indexInput]; // DEBUG: Draw square Gl.glPushMatrix(); //Gl.glDisable(Gl.GL_LIGHTING); //Gl.glTranslatef(-0.5f, -0.5f, 0.0f); Gl.glBindTexture(Gl.GL_TEXTURE_2D, m_InputAsSliceTexId); Gl.glBegin(Gl.GL_QUADS); { //Gl.glColor3fv( siColorHelper.To3fv(Color.AliceBlue) ); Gl.glTexCoord2d(0, 0); Gl.glVertex2d(-1, -1); // Bottom Left Gl.glTexCoord2d(1, 0); Gl.glVertex2d( 1, -1); // Bottom Right Gl.glTexCoord2d(1, 1); Gl.glVertex2d( 1, 1); // Top Right Gl.glTexCoord2d(0, 1); Gl.glVertex2d(-1, 1); // Top Left } Gl.glEnd(); //Gl.glEnable(Gl.GL_LIGHTING); Gl.glPopMatrix(); // Flush Gl.glFlush(); } //===================================================================== #endregion #region Event Handler Methods //===================================================================== protected override void OnMouseDown(MouseEventArgs e) { base.OnMouseDown(e); // Check we have rendered if (!this.m_HasRenderedFirstTime) return; // Save the last screen mouse down this.Metadata["LastScreenMouseDown"] = e; if ((bool)this.Metadata["IsMouseInsideImageSpace"]) { // Convert the screen mouse location to image physical location RectangleF rectImageScreen = (RectangleF)this.Metadata["ImageScreenRectangle"]; this.Metadata["LastImagePointMouseDown"] = this.Metadata["LastImagePointMouseMove"]; this.Metadata["LastImageIndexMouseDown"] = this.Metadata["LastImageIndexMouseMove"]; this.Metadata["LastImagePixelMouseDown"] = this.Metadata["LastImagePixelMouseMove"]; } // Change Metadata if required if (e.Button == MouseButtons.Right) { this.Metadata["IsTranslatingImage"] = true; this.Cursor = Cursors.Hand; } } protected override void OnMouseUp(MouseEventArgs e) { base.OnMouseUp(e); // Change is we are translating if (e.Button == MouseButtons.Right) this.Metadata["IsTranslatingImage"] = false; } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); // Check we have rendered if (!this.m_HasRenderedFirstTime) return; // Convert the screen mouse location to image pixel location RectangleF rectImageScreen = (RectangleF)this.Metadata["ImageScreenRectangle"]; itk.itkIndex index = this.TransformScreenPointToImageIndex(e.Location, rectImageScreen); // Save the old cursor value Cursor newCursor = this.Cursor; // Check the Metadata of the mouse move if ((bool)this.Metadata["IsTranslatingImage"]) { // We are translating the image MouseEventArgs last = this.Metadata["LastScreenMouseMove"] as MouseEventArgs; newCursor = Cursors.Hand; PointF newOffset = new PointF(this.Offset.X, this.Offset.Y); newOffset.X += (1.0F / (float)this.ZoomFactor * (float)(e.X - last.X)); newOffset.Y += (1.0F / (float)this.ZoomFactor * (float)(e.Y - last.Y)); this.Offset = newOffset; this.Repaint(); } else if (this.InputAsImage(0).LargestPossibleRegion.IsInside(index)) { // The location is inside the image // Convert the screen mouse location to image physical location itk.itkPoint point = this.TransformScreenPointToImagePoint(e.Location, rectImageScreen); // Update the Metadata this.Metadata["LastImagePointMouseMove"] = point; this.Metadata["LastImageIndexMouseMove"] = index; this.Metadata["LastImagePixelMouseMove"] = this.InputAsImage(0).GetPixel(index); this.Metadata["IsMouseInsideImageSpace"] = true; newCursor = Cursors.Cross; } else { // The location is outside the image this.Metadata["IsMouseInsideImageSpace"] = false; newCursor = Cursors.Default; } // Set the form cursor if (this.Cursor != Cursors.WaitCursor) { if (this.ContainsMetadata("Cursor")) this.Cursor = (Cursor)this.Metadata["Cursor"]; else this.Cursor = newCursor; } // Save the last screen mouse move this.Metadata["LastScreenMouseMove"] = e; } protected override void OnMouseWheel(MouseEventArgs e) { if (e.Delta > 0) this.ZoomOutByFactorOfTwo(); else this.ZoomInByFactorOfTwo(); this.Repaint(); } protected override void OnKeyDown(KeyEventArgs e) { if (e.KeyCode == Keys.Add || e.KeyCode == Keys.Oemplus) this.ZoomInByFactorOfTwo(); else if (e.KeyCode == Keys.Subtract || e.KeyCode == Keys.OemMinus) this.ZoomOutByFactorOfTwo(); } void OpenGlSliceRenderer_SliceChanged(siOpenGlSliceRenderer renderer, siSliceChangedEventArgs e) { if (e.ScrollArgs.Type != ScrollEventType.EndScroll) this.m_ToolTipSlice.Show(e.Slice.ToString("#000"), e.Window, e.Location, 350); else this.m_ToolTipSlice.Hide(e.Window); } void m_SliceSlider_Scroll(object sender, ScrollEventArgs e) { if (!this.m_HasRenderedFirstTime) return; if (e.Type == ScrollEventType.EndScroll) return; // Update the slice int slice = e.NewValue; this.Metadata["CurrentSlice"] = slice; for (int i = 0; i < this.Inputs.Count; i++) this.UpdateInputAsSlice(i); // Caculate the position near the slider control // NOTE: The ScrollBar control does not have a way to get // the mouse position during a change so we must // do this dodgy calculation... int controlBoxHeight = 20; int y = (int)((double)(this.m_SliceSlider.Height - controlBoxHeight) * (double)this.m_SliceSlider.Value / (double)this.m_SliceSlider.Maximum); y += controlBoxHeight + 10; int x = this.m_SliceSlider.Left + this.m_SliceSlider.Width + 10; // Raise the SliceChanged event siSliceChangedEventArgs args = new siSliceChangedEventArgs(this.Form, new Point(x, y), e, slice); this.FireSliceChanged(args); // Force a repaint this.Repaint(); } void m_SliceSlider_MouseEvent(object sender, EventArgs e) { this.m_SliceSlider.Cursor = Cursors.Default; } //===================================================================== #endregion #region Rendering Helper Methods //===================================================================== /// /// Returns the given input index as an itk.itkImage. /// Throws an exception if the index is invalid or the input is not /// an itk.itkImageBase. /// /// /// private itk.itkImageBase InputAsImage(int index) { // Check the input is supported if (this.Inputs.Count < index || this.Inputs[index] == null) throw new ApplicationException("The " + this.TypeName + " Input[" + index.ToString() + "] is invalid."); else if (this.Inputs[index] is itk.itkImageBase) // Return return this.Inputs[index] as itk.itkImageBase; else throw new NotImplementedException("The " + this.TypeName + " does not support the type of Input[" + index.ToString() + "]."); } /// /// Initialise each input image. /// private void InitialiseInputs() { for (int i = 0; i < this.Inputs.Count; i++) this.UpdateInputAsSlice(i); } /// /// Update the sliced input. /// /// private void UpdateInputAsSlice(int index) { // Create the list of image slices while (this.InputsAsSlice.Count < this.Inputs.Count) this.InputsAsSlice.Add(null); // Get the input as a slice itk.itkImageBase input = this.InputAsImage(index); if (input.Dimension == 2) { this.InputsAsSlice[index] = input; } else if (input.Dimension == 3) { // Create the slice image itk.itkImageBase inputAsSlice = itk.itkImage.New(input.PixelType, 2); if (this.m_ExtractFilters.Count != this.Inputs.Count) { this.m_ExtractFilters.Clear(); for (int i = 0; i < this.Inputs.Count; i++) { this.m_ExtractFilters.Add(itk.itkExtractImageFilter.New(input, inputAsSlice)); this.m_ExtractFilters[i].RemoveAllObservers(); } } else { // Dispose of the old slice if (this.InputsAsSlice[index] != null) { this.InputsAsSlice[index].DisconnectPipeline(); this.InputsAsSlice[index].Dispose(); } } // Update the slice int slice = (int)this.Metadata["CurrentSlice"]; this.m_ExtractFilters[index].SetInput(input); this.m_ExtractFilters[index].ExtractSlice(2, slice); this.m_ExtractFilters[index].Update(); this.m_ExtractFilters[index].GetOutput(inputAsSlice); // Store slice image this.InputsAsSlice[index] = inputAsSlice; this.InputsAsSlice[index].Name = input.Name + " (Slice" + slice.ToString() + ")"; } // Load slice as texture Gl.glGenTextures(1, new IntPtr(m_InputAsSliceTexId)); Gl.glBindTexture(Gl.GL_TEXTURE_2D, m_InputAsSliceTexId); Gl.glTexImage2D(Gl.GL_TEXTURE_2D, 0, Gl.GL_LUMINANCE8, this.InputsAsSlice[index].Size[0], this.InputsAsSlice[index].Size[1], 0, Gl.GL_LUMINANCE, Gl.GL_UNSIGNED_BYTE, this.InputsAsSlice[index].Buffer ); 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_NEAREST); //GL_LINEAR Gl.glTexParameteri(Gl.GL_TEXTURE_2D, Gl.GL_TEXTURE_MIN_FILTER, Gl.GL_NEAREST); //GL_LINEAR Gl.glPixelStorei(Gl.GL_UNPACK_ALIGNMENT, 1); } /// /// Modify the ZoomFactor to zoom in x2. /// public void ZoomInByFactorOfTwo() { this.ZoomFactor *= 2.0; if (this.ZoomFactor > 64) this.ZoomFactor = 64; } /// /// Modify the ZoomFactor to zoom out x2. /// public void ZoomOutByFactorOfTwo() { this.ZoomFactor /= 2.0; if (this.ZoomFactor < 0.015625) this.ZoomFactor = 0.015625; } //===================================================================== #endregion #region Point Coordinate Transform Helper Methods //===================================================================== protected itk.itkPoint TransformScreenPointToImagePoint( Point screen, RectangleF rectImageScreen ) { // Convert the screen point to an image point (first 2 dimensions) itk.itkImageBase input = this.InputAsImage(0); itk.itkPoint point; input.TransformIndexToPhysicalPoint(this.TransformScreenPointToImageIndex(screen, rectImageScreen), out point); return point; } protected itk.itkIndex TransformScreenPointToImageIndex( Point screen, RectangleF rectImageScreen ) { // Convert the screen location to an image index itk.itkImageBase input = this.InputAsImage(0); itk.itkContinuousIndex cindex = new itk.itkContinuousIndex(input.Dimension); cindex[0] = ((double)screen.X - (double)rectImageScreen.X) / this.ZoomFactor; cindex[1] = (((double)screen.Y - (double)rectImageScreen.Y) / this.ZoomFactor) / (input.Spacing[1] / input.Spacing[0]); if (cindex.Dimension == 3) cindex[2] = (double)((int)this.Metadata["CurrentSlice"]); return cindex.ToIndex(); } public PointF TransformImageContinuousIndexToScreenPoint(itk.itkContinuousIndex cindex, RectangleF rectImageScreen ) { // Convert the screen location to an image index itk.itkImageBase input = this.InputAsImage(0); PointF screenPoint = new PointF(); screenPoint.X = (float)((cindex[0] * this.ZoomFactor) + (double)rectImageScreen.X); screenPoint.Y = (float)((cindex[1] * this.ZoomFactor) * (input.Spacing[1] / input.Spacing[0]) + (double)rectImageScreen.Y); return screenPoint; } public PointF TransformImageIndexToScreenPoint(itk.itkIndex index, RectangleF rectImageScreen ) { itk.itkContinuousIndex cindex = new itk.itkContinuousIndex(index); return this.TransformImageContinuousIndexToScreenPoint(cindex, rectImageScreen); } public PointF TransformImagePointToScreenPoint(itk.itkImageBase image, itk.itkPoint point, RectangleF rectImageScreen ) { itk.itkContinuousIndex cindex; image.TransformPhysicalPointToContinuousIndex(point, out cindex); return this.TransformImageContinuousIndexToScreenPoint(cindex, rectImageScreen); } //===================================================================== #endregion } //========================================================================= #endregion }