/*============================================================================= Project: SharpImage Module: siTransferFunction.cs Language: C# Author: Dan Mueller Date: $Date: 2008-01-09 16:23:10 +1000 (Wed, 09 Jan 2008) $ Revision: $Revision: 29 $ 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.Text; using System.Drawing; using System.Drawing.Imaging; using System.Drawing.Drawing2D; using System.Collections.Generic; using System.Diagnostics; using System.Windows.Forms; using System.Xml.Serialization; namespace SharpImage.Rendering { #region TransferFunctionPaintMode Enumeration //========================================================================= /// /// Specifies the painting mode for the transfer function. /// public enum siTransferFunctionPaintMode { /// /// Paint the transfer function for use on the screen. /// Screen, /// /// Paint the transfer function for use in a publication. /// Publish, /// /// Paint the transfer function for use as a LUT function. /// Function } //========================================================================= #endregion #region siTransferFunctionPaintEventArgs Class //========================================================================= public class siTransferFunctionPaintEventArgs : PaintEventArgs { private siTransferFunctionPaintMode m_PaintMode; private String m_Layer; /// /// Default constructor. /// /// The graphics handle to paint to. /// /// The mode of the painting: eg. Screen or Function. /// The key of the layer to paint. Null or empty to paint all layers. public siTransferFunctionPaintEventArgs(Graphics g, Rectangle clipRect, siTransferFunctionPaintMode mode, String layer) : base(g, clipRect) { this.m_PaintMode = mode; this.m_Layer = layer; } /// /// Constructor taking PaintEventArgs. /// /// /// /// public siTransferFunctionPaintEventArgs(PaintEventArgs e, siTransferFunctionPaintMode mode, String layer) : this(e.Graphics, e.ClipRectangle, mode, layer) { // Nothing to do } /// /// Constructor taking PaintEventArgs and drawing all layers. /// /// /// public siTransferFunctionPaintEventArgs(PaintEventArgs e, siTransferFunctionPaintMode mode) : this(e.Graphics, e.ClipRectangle, mode, null) { // Nothing to do } /// /// Get the paint mode ie. Screen for edit mode OR Fuction for actual usage. /// public siTransferFunctionPaintMode PaintMode { get { return this.m_PaintMode; } } /// /// Get the name/key of the layer to draw. /// If null or empty, all layers are drawn. /// public String Layer { get { return this.m_Layer; } } } //========================================================================= #endregion /// /// Represents a two dimensional transfer function (usually Value-GradMag) /// for direct volume rendering. The transfer function comprises of a user /// specified number of layers. As such the TF is represented as a 3D image, /// with each slice depicting each layer. Each layer is actually duplicated /// and allow correct interpolation to occur between the duplicated slices. /// Transfer functions are always represented with RGBA pixel type. /// public class siTransferFunction : IDisposable { #region Constants //===================================================================== public const string UNUSED_LAYER_KEY = "Unused"; private int RGBA_CHANNELS = 4; private PixelFormat RGBA_FORMAT = PixelFormat.Format32bppArgb; //===================================================================== #endregion #region Instance Variables //===================================================================== private bool m_Disposed = false; //===================================================================== #endregion #region Construction and Disposal //===================================================================== /// /// Protected parameterless constructor for serialization. /// protected siTransferFunction() { } /// /// Default constructor for a single layer transfer function. /// /// The size of the 2D transfer functions. public siTransferFunction(itk.itkSize size) { this.m_Size0 = size[0]; this.m_Size1 = size[1]; List layers = new List(1); layers.Add("Main"); this.Initialise(size, layers); } /// /// Constructor. /// /// The size of the 2D transfer functions. /// A list of names or keys for the layers of the transfer function. public siTransferFunction(itk.itkSize size, params String[] layers) : this(size, new List(layers)) { } /// /// Constructor. /// /// The size of the 2D transfer functions. /// A list of names or keys for the layers of the transfer function. public siTransferFunction(itk.itkSize size, List layers) { this.m_Size0 = size[0]; this.m_Size1 = size[1]; this.Initialise(size, layers); } /// /// Initialise the transfer function. /// protected void Initialise(itk.itkSize size, List layers) { // Check size is 2D if (size.Dimension != 2) throw new ArgumentOutOfRangeException("size", size, "Invalid size: the TransferFuction size must have two dimensions."); // Check that size is a power of two for (int i = 0; i < size.Dimension; i++) { int roundedSize = siMathHelper.RoundToNextHighestPowerOfTwo(size[i]); if (roundedSize != size[i]) throw new ArgumentOutOfRangeException("size", size, "Invalid size: the TransferFuction size must be a power of two (eg. 256 x 256)."); } // Expand the layers to a power of two int roundedLayers = siMathHelper.RoundToNextHighestPowerOfTwo(layers.Count); int diffLayers = roundedLayers - layers.Count; for (int i = 0; i < diffLayers; i++) layers.Add(UNUSED_LAYER_KEY); // Expand the size to include the layers itk.itkSize sizeWithLayers = new itk.itkSize(size[0], size[1], layers.Count); // Set the layers this.m_Layers = layers; // Create the underlying image itk.itkIndex index = new itk.itkIndex(3U); itk.itkImageRegion region = new itk.itkImageRegion(sizeWithLayers, index); this.m_Image = itk.itkImage_RGBAUC3.New(); this.m_Image.SetRegions(region); this.m_Image.Allocate(); itk.itkPixel_RGBAUC zero = new itk.itkPixel_RGBAUC(new itk.itkArray((uint)RGBA_CHANNELS)); this.m_Image.FillBuffer(zero); this.m_Image.Spacing = new itk.itkSpacing(1.0, 1.0, 1.0); this.m_Image.Name = null; // Create the bitmaps to access the functions this.m_Bitmaps = new Bitmap[sizeWithLayers[2]]; for (int k = 0; k < sizeWithLayers[2]; k++) { int iOffset = k * sizeWithLayers[1] * sizeWithLayers[0] * RGBA_CHANNELS; int iBufferWithOffset = this.m_Image.Buffer.ToInt32() + iOffset; IntPtr bufferWithOffset = new IntPtr(iBufferWithOffset); this.m_Bitmaps[k] = new Bitmap(sizeWithLayers[0], sizeWithLayers[1], sizeWithLayers[0] * RGBA_CHANNELS, RGBA_FORMAT, bufferWithOffset); } // Setup default Metadata this.Metadata["Cursor"] = Cursors.Default; } /// /// Dispose of the siTransferFunction resources. /// public void Dispose() { // Only dispose once if (this.m_Disposed) return; else this.m_Disposed = true; // Dispose of bitmaps if (this.m_Bitmaps != null && this.m_Bitmaps.Length > 0) { foreach (Bitmap bitmap in this.m_Bitmaps) bitmap.Dispose(); this.m_Bitmaps = null; } // Dispose of the image if (this.m_Image != null) this.m_Image.Dispose(); // Clear the parts list if (this.Parts != null) this.Parts.Clear(); // Tell the GC that the Finalize process no longer needs // to be run for this object GC.SuppressFinalize(this); } //===================================================================== #endregion #region Properties //===================================================================== #region Metadata //===================================================================== private Dictionary m_Metadata = new Dictionary(); /// /// Gets the Metadata variable list. /// [System.Xml.Serialization.XmlIgnore] public Dictionary Metadata { get { return this.m_Metadata; } } //===================================================================== #endregion #region Layers //===================================================================== private List m_Layers = new List(); /// /// Gets the list of layers within the transfer function. /// public List Layers { get { return this.m_Layers; } } //===================================================================== #endregion #region Size //===================================================================== private int m_Size0 = 0; private int m_Size1 = 0; /// /// This method is needed for serialization. /// It is not intended for general use. /// public int Size0 { get { return this.m_Size0; } set { this.m_Size0 = value; } } /// /// This method is needed for serialization. /// It is not intended for general use. /// public int Size1 { get { return this.m_Size1; } set { this.m_Size1 = value; } } //===================================================================== #endregion #region Image //===================================================================== private itk.itkImageBase m_Image = null; /// /// Get the 3D image representing the layers of 2D transfer functions. /// [System.Xml.Serialization.XmlIgnore] public itk.itkImageBase Image { get { return this.m_Image; } } //===================================================================== #endregion #region Bitmaps //===================================================================== private Bitmap[] m_Bitmaps = null; /// /// Get the 3D image representing the layers of 2D transfer functions. /// [System.Xml.Serialization.XmlIgnore] protected Bitmap[] Bitmaps { get { return this.m_Bitmaps; } } //===================================================================== #endregion #region Parts //===================================================================== private List m_Parts = new List(); /// /// Gets the list of parts comprising the transfer function. /// [XmlElement(Type = typeof(siTransferFunctionPartRectangle)), XmlElement(Type = typeof(siTransferFunctionPartOval)), XmlElement(Type = typeof(siTransferFunctionPartGradient)), XmlElement(Type = typeof(siTransferFunctionPartLevoy)), XmlElement(Type = typeof(siTransferFunctionPartTrapezoid))] public List Parts { get { return this.m_Parts; } } //===================================================================== #endregion #region Modified //===================================================================== private bool m_IsModified = false; private bool m_ModifiedEventEnabled = true; /// /// Gets/sets if the Renderer has been modified and needs repainting. /// [System.Xml.Serialization.XmlIgnore] public bool IsModified { get { return this.m_IsModified; } set { this.m_IsModified = value; } } /// /// Gets/sets if the transfer function raises modified events when /// the function is changed. /// [System.Xml.Serialization.XmlIgnore] public bool ModifiedEventEnabled { get { return this.m_ModifiedEventEnabled; } set { this.m_ModifiedEventEnabled = value; if (value) this.RaiseModified(); } } //===================================================================== #endregion #region Background //===================================================================== private itk.itkImageBase m_BackgroundImage = null; private Bitmap m_BackgroundBitmap = null; /// /// Gets/sets the background image as an itkImageBase. /// [System.Xml.Serialization.XmlIgnore] public itk.itkImageBase BackgroundImage { get { return this.m_BackgroundImage; } set { this.m_BackgroundImage = value; } } //===================================================================== #endregion //===================================================================== #endregion #region Events and Delegates //===================================================================== public delegate void siTransferFunctionHandler(siTransferFunction sender); public delegate void siTransferFunctionModifiedHandler(siTransferFunction sender, bool partial); #region Modified //===================================================================== private siTransferFunctionModifiedHandler m_EventStorage_Modified; /// /// An event raised when the Transfer Function is modified. /// public event siTransferFunctionModifiedHandler Modified { add { this.m_EventStorage_Modified += value; } remove { this.m_EventStorage_Modified -= value; } } /// /// Raises the Modified event. Signals a full modification (ie. partial = false). /// protected void RaiseModified() { if (this.m_EventStorage_Modified != null && this.ModifiedEventEnabled) this.m_EventStorage_Modified(this, false); } /// /// Raises the Modified event. Signals a full or partial modification. /// protected void RaiseModified(bool partial) { if (this.m_EventStorage_Modified != null && this.ModifiedEventEnabled) this.m_EventStorage_Modified(this, partial); } //===================================================================== #endregion #region EditStarted //===================================================================== private siTransferFunctionHandler m_EventStorage_EditStarted; /// /// An event raised when the Transfer Function is starting being edited. /// public event siTransferFunctionHandler EditStarted { add { this.m_EventStorage_EditStarted += value; } remove { this.m_EventStorage_EditStarted -= value; } } /// /// Raises the EditStarted event. /// protected void RaiseEditStarted() { if (this.m_EventStorage_EditStarted != null) this.m_EventStorage_EditStarted(this); } //===================================================================== #endregion #region EditFinished //===================================================================== private siTransferFunctionHandler m_EventStorage_EditFinished; /// /// An event raised when the Transfer Function is finished being edited. /// public event siTransferFunctionHandler EditFinished { add { this.m_EventStorage_EditFinished += value; } remove { this.m_EventStorage_EditFinished -= value; } } /// /// Raises the EditFinished event. /// protected void RaiseEditFinished() { if (this.m_EventStorage_EditFinished != null) this.m_EventStorage_EditFinished(this); } //===================================================================== #endregion #region PartSelected //===================================================================== private siActor.siActorHandler m_EventStorage_PartSelected; /// /// An event raised when a transfer function part is selected (clicked). /// public event siActor.siActorHandler PartSelected { add { this.m_EventStorage_PartSelected += value; } remove { this.m_EventStorage_PartSelected -= value; } } /// /// Raises the PartSelected event. /// protected void RaisePartSelected(siTransferFunctionPart part) { if (this.m_EventStorage_PartSelected != null) this.m_EventStorage_PartSelected(part as siActor); } //===================================================================== #endregion //===================================================================== #endregion #region Serialization Methods //===================================================================== /// /// Serializes this object to the given path. /// /// The absolute path and name of the file to create with the serialized object. public void ToXmlFile(string filePath) { // Ensure the filePath is valid if (filePath == null || filePath.Length == 0) throw new ArgumentNullException("filePath", "The file path to serialize the Transfer Function can not be null or empty."); // Ensure the directory exists if (!Directory.Exists(Path.GetDirectoryName(filePath))) Directory.CreateDirectory(Path.GetDirectoryName(filePath)); // Serialize XmlSerializer serializer = new XmlSerializer(typeof(siTransferFunction)); FileStream stream = File.Create(filePath); serializer.Serialize(stream, this); stream.Flush(); stream.Close(); stream.Dispose(); } /// /// Deserializes this object from the given path. /// /// The absolute path and name of the file to deserialize the object from. /// public static siTransferFunction FromXmlFile(string filePath) { // Ensure the filePath is valid if (filePath == null || filePath.Length == 0) throw new ArgumentNullException("filePath", "The file path to serialize the Transfer Function can not be null or empty."); if (!File.Exists(filePath)) throw new FileNotFoundException("The file path to deserialize the Transfer Function was not found.", filePath); // Deserialize XmlSerializer serializer = new XmlSerializer(typeof(siTransferFunction)); FileStream stream = File.OpenRead(filePath); siTransferFunction result = (siTransferFunction)serializer.Deserialize(stream); // Finish setting up result result.Initialise(new itk.itkSize(result.Size0, result.Size1), result.Layers); siTransferFunctionPart[] parts = result.Parts.ToArray(); result.Parts.Clear(); foreach (siTransferFunctionPart part in parts) result.AddPartToFront(part); // Clean up stream.Flush(); stream.Close(); stream.Dispose(); // Return return result; } //===================================================================== #endregion #region Paint Methods //===================================================================== /// /// Paint the transfer function layer in the given mode to the graphics handle. /// If e.Layer is null or empty, all the layers are painted. /// /// public void Paint(siTransferFunctionPaintEventArgs e) { // Set to draw smooth e.Graphics.SmoothingMode = SmoothingMode.AntiAlias; // Paint the background this.PaintBackground(e); // Paint each part in reverse order for (int i = this.Parts.Count - 1; i >= 0; i--) this.Parts[i].Paint(this, e); // Clear the modified flag this.IsModified = false; } /// /// Paint the transfer function layers to the internal 3D image. /// public void PaintToImage() { for (int i = 0; i < this.Bitmaps.Length; i++) { // Setup for painting Graphics g = Graphics.FromImage(this.Bitmaps[i]); Rectangle clipRect = new Rectangle(0, 0, this.Image.Size[0], this.Image.Size[1]); g.Clip = new Region(clipRect); siTransferFunctionPaintMode mode = siTransferFunctionPaintMode.Function; siTransferFunctionPaintEventArgs e = new siTransferFunctionPaintEventArgs(g, clipRect, mode, this.Layers[i]); // Paint background this.PaintBackground(e); // Paint each part in reverse order for (int j = this.Parts.Count - 1; j >= 0; j--) this.Parts[j].Paint(this, e); // Clean up g.Dispose(); e.Dispose(); } } /// /// Returns a bitmap which represents a "screen capture". /// /// public virtual Bitmap GetScreenCapture() { // Create and setup the output bitmap for offscreen rendering Bitmap result = new Bitmap(this.Image.Size[0]+1, this.Image.Size[1]+1); Graphics g = Graphics.FromImage(result); g.Clear(Color.White); g.SmoothingMode = SmoothingMode.AntiAlias; Rectangle clipRect = new Rectangle(0, 0, this.Image.Size[0]+1, this.Image.Size[1]+1); g.Clip = new Region(clipRect); siTransferFunctionPaintMode mode = siTransferFunctionPaintMode.Publish; siTransferFunctionPaintEventArgs e = new siTransferFunctionPaintEventArgs(g, clipRect, mode, null); // Paint background this.PaintBackground(e); // Paint each part in reverse order for (int j = this.Parts.Count - 1; j >= 0; j--) this.Parts[j].Paint(this, e); // Clean up g.Dispose(); e.Dispose(); // Return return result; } //===================================================================== #endregion #region Mouse Methods //===================================================================== public void MouseDown(MouseEventArgs e) { // Allow the Parts to consume the event bool eventWasConsumed = false; foreach (siTransferFunctionPart part in this.Parts.ToArray()) { eventWasConsumed = part.MouseDown(this, e); if (eventWasConsumed) { this.SendPartToFront(part); this.RaisePartSelected(part); this.RaiseEditStarted(); return; } } } public void MouseUp(MouseEventArgs e) { // Allow the Parts to consume the event bool eventWasConsumed = false; foreach (siTransferFunctionPart part in this.Parts.ToArray()) { eventWasConsumed = part.MouseUp(this, e); if (eventWasConsumed) { this.RaiseEditFinished(); return; } } } public void MouseMove(MouseEventArgs e) { // Allow the Parts to consume the event bool eventWasConsumed = false; foreach (siTransferFunctionPart part in this.Parts.ToArray()) { eventWasConsumed = part.MouseMove(this, e); if (eventWasConsumed) return; } if (!eventWasConsumed) { if (this.Metadata["Cursor"] as Cursor != Cursors.Default) { this.Metadata["Cursor"] = Cursors.Default; this.RaiseModified(); } } } //===================================================================== #endregion #region Part Methods //===================================================================== /// /// Adds the given part to the function at the back of the list. /// /// public void AddPart(siTransferFunctionPart part) { if (part != null) this.AddPartToBack(part); } /// /// Adds the given part to the front of the list. /// /// public void AddPartToFront(siTransferFunctionPart part) { if (part != null) { this.Parts.Insert(0, part); part.Modified += new siActor.siActorModifiedHandler(Part_Modified); part.RaiseModified(); this.RaisePartSelected(part); } } /// /// Adds the given part to the back of the list. /// /// public void AddPartToBack(siTransferFunctionPart part) { if (part != null) { this.Parts.Add(part); part.Modified += new siActor.siActorModifiedHandler(Part_Modified); part.RaiseModified(); this.RaisePartSelected(part); } } /// /// Removes the given part from the list. /// /// public void RemovePart(siTransferFunctionPart part) { if (part != null && this.Parts.Contains(part)) { this.Parts.Remove(part); part.Modified -= new siActor.siActorModifiedHandler(Part_Modified); this.Metadata["Cursor"] = Cursors.Default; this.IsModified = true; this.RaiseModified(false); } } /// /// Remove the part at the front of the list (the currently selected part). /// public void RemovePartAtFront() { if (this.Parts != null && this.Parts.Count > 0) this.RemovePart(this.Parts[0]); } /// /// Remove all the parts from the list. /// public void ClearAllParts() { siTransferFunctionPart[] parts = this.Parts.ToArray(); foreach (siTransferFunctionPart part in parts) this.RemovePart(part); } /// /// Sends the given part to the back of the list. /// /// public void SendPartToBack(siTransferFunctionPart part) { if (part != null && this.Parts.Contains(part)) { this.Parts.Remove(part); this.Parts.Add(part); part.RaiseModified(); } } /// /// Sends the given part the front of the list. /// /// public void SendPartToFront(siTransferFunctionPart part) { if (part != null && this.Parts.Contains(part)) { this.Parts.Remove(part); this.Parts.Insert(0, part); part.RaiseModified(); this.RaisePartSelected(part); } } /// /// An event handler for whenever a part is modified. /// /// private void Part_Modified(siActor sender, bool partial) { // Indicate the function has been modified this.IsModified = true; this.RaiseModified(partial); } //===================================================================== #endregion #region Private Methods //===================================================================== /// /// Paint the background of the transfer function to g depending on /// the mode: for Screen mode paint a checker board effect to indicate /// transparency, for Function mode clear the background with a /// transparent color. /// /// /// private void PaintBackground(siTransferFunctionPaintEventArgs e) { switch (e.PaintMode) { case siTransferFunctionPaintMode.Screen: // Fall through case siTransferFunctionPaintMode.Publish: // Get background bitmap Bitmap background = this.GetBackgroundBitmap(); e.Graphics.DrawImageUnscaled(background, 0, 0); break; case siTransferFunctionPaintMode.Function: // Clear background with transparent color e.Graphics.Clear(Color.FromArgb(0, 0, 0, 0)); break; } } /// /// Construct the background bitmap (or return an already constructed bitmap). /// private Bitmap GetBackgroundBitmap() { if (this.m_BackgroundBitmap != null) { // Do nothing, we have already constructed the bitmap } else if (this.m_BackgroundImage != null) { // The user has specified an image, convert to a Bitmap if (this.m_BackgroundImage.Dimension != 2 || this.m_BackgroundImage.PixelType.IsArray || this.m_BackgroundImage.PixelType.IsSigned || !this.m_BackgroundImage.PixelType.IsChar) { // The given background image is not valid, use the default } else { // Convert from itk to bitmap // NOTE: On failure, null is returned and the default used. this.m_BackgroundBitmap = this.ConvertItkImageToBitmap(this.m_BackgroundImage); } } // Check that a valid bitmap was created if (this.m_BackgroundBitmap == null) { // Use the checker board default this.m_BackgroundBitmap = siColorHelper.CreateTransparentBackground(this.Image.Size[0] + 1, this.Image.Size[1] + 1); } // Return return this.m_BackgroundBitmap; } /// /// Converts a UC2 itkImage to a bitmap. /// This method does not check that the given image is of type UC2. /// On failure of any kind, null is returned. /// /// /// private Bitmap ConvertItkImageToBitmap(itk.itkImageBase image) { Bitmap result = null; try { // Create bitmap palette and format siGdiLookupTable lut = new siGdiLookupTable(); lut.SetTableRange(image.PixelType); lut.SetTableToGreyscale(); ColorPalette palette = palette = lut.GetGdiPalette(); PixelFormat format = lut.GetGdiPixelFormat(); // Check if the stride is the same as the width if (image.Size[0] % 4 == 0) { // Width = Stride: simply use the Bitmap constructor result = new Bitmap(image.Size[0], // Width image.Size[1], // Height image.Size[0], // Stride format, // PixelFormat image.Buffer // Buffer ); } else { unsafe { // Width != stride: copy data from buffer to bitmap int width = image.Size[0]; int height = image.Size[1]; byte* buffer = (byte*)image.Buffer.ToPointer(); // Compute the stride int stride = width; if (image.Size[0] % 4 != 0) stride = ((image.Size[0] / 4) * 4 + 4); result = new Bitmap(width, height, format); Rectangle rect = new Rectangle(0, 0, width, height); BitmapData bitmapData = result.LockBits(rect, ImageLockMode.WriteOnly, format); for (int k = 0; k < height; k++) // Row { byte* row = (byte*)bitmapData.Scan0 + (k * stride); for (int j = 0; j < width; j++) // Column { row[j] = buffer[(k * width) + j]; } } result.UnlockBits(bitmapData); } // end unsafe } // Set the palette result.Palette = palette; } catch { // Null the result result = null; } // Return return result; } //===================================================================== #endregion } }