/*=============================================================================
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
}