/*============================================================================= Project: SharpImage Module: siGdiSliceRenderer.cs Language: C# Author: Dan Mueller Date: $Date: 2007-07-06 10:57:00 +1000 (Fri, 06 Jul 2007) $ Revision: $Revision: 2 $ 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 SharpImage.Main; namespace SharpImage.Rendering { #region SliceChangedEventArgs Class //========================================================================= public class siSliceChangedEventArgs : EventArgs { private IWin32Window m_Window; private Point m_Location; private ScrollEventArgs m_ScrollArgs; private int m_Slice; public siSliceChangedEventArgs(IWin32Window window, Point location, ScrollEventArgs scrollArgs, int slice) : base() { m_Window = window; m_Location = location; m_ScrollArgs = scrollArgs; m_Slice = slice; } public IWin32Window Window { get { return this.m_Window; } } public Point Location { get { return this.m_Location; } } public ScrollEventArgs ScrollArgs { get { return this.m_ScrollArgs; } } public int Slice { get { return this.m_Slice; } } } //========================================================================= #endregion #region siGdiSliceRenderer Class //========================================================================= /// /// Renders 2-D and 3-D images as slices along the z-axis using the Windows /// GDI+ (System.Drawing) 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 siGdiSliceRenderer : siRenderer, IDisposable { #region Constants //===================================================================== protected static readonly Font s_ImageInfoFont = new Font("Arial", 9.0F); protected static readonly SolidBrush s_ImageInfoBrush = new SolidBrush(Color.Black); protected static readonly Color s_ImageBorderColor = Color.Black; protected static readonly Color s_BackColor = Color.White; protected const int SPACE_AROUND_IMAGE = 6; protected const double MINIMUM_SCALE_FACTOR = 0.25; //===================================================================== #endregion #region Instance Variables //===================================================================== private bool m_HasRenderedFirstTime = false; private bool m_IsDisposed = false; private ToolTip m_ToolTipSlice; private VScrollBar m_SliceSlider; //===================================================================== #endregion #region Construction and Disposal //===================================================================== /// /// Public constructor. /// public siGdiSliceRenderer(IApplication parent) : base("GDI Viewer", parent, new siGdiRendererForm()) { // Setup form this.BackColor = s_BackColor; // Setup Metadata this.Metadata["IsTranslatingImage"] = false; this.Metadata["IsMouseInsideImageSpace"] = false; this.Metadata["ZoomFactor"] = (double)1.0; this.Metadata["PaintBorder"] = true; this.Metadata["ArrayIndex"] = 0U; // 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(GdiSliceRenderer_SliceChanged); } /// /// Deconstructor. /// ~siGdiSliceRenderer() { // 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; // TODO: At the moment, if an image is a label it is NEVER disposed of // by this method (it must rely on reference counting to dispose // of it automatically when it is no longer referenced). // Only get here if not already disposed if (disposeManagedResources) { // Clean up bitmaps if (this.InputsAsBitmap != null) { foreach (Bitmap inputAsBitmap in this.InputsAsBitmap) { if (inputAsBitmap != null) inputAsBitmap.Dispose(); } this.InputsAsBitmap.Clear(); this.m_InputsAsBitmap = null; } // Clean up scalar images if (this.InputsAsScalar != null) { foreach (itk.itkImageBase input in this.InputsAsScalar) { if (input != null && !input.IsDisposed && !input.Metadata.ContainsKey("IsLabel")) { input.DisconnectPipeline(); input.Dispose(); } } this.InputsAsScalar.Clear(); this.m_InputsAsScalar = null; } // Clean up slice images if (this.InputsAsSlice != null) { foreach (itk.itkImageBase input in this.InputsAsSlice) { if (input != null && !input.IsDisposed && !input.Metadata.ContainsKey("IsLabel")) { input.DisconnectPipeline(); input.Dispose(); } } this.InputsAsSlice.Clear(); this.m_InputsAsSlice = null; } // Clean up input images if (this.Inputs != null) { foreach (itk.itkImageBase input in this.Inputs) { if (input != null && !input.IsDisposed && !input.Metadata.ContainsKey("IsLabel")) { input.DisconnectPipeline(); input.Dispose(); } } this.Inputs.Clear(); } // Clean up slider 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 Slice //===================================================================== /// /// Gets/sets the current slice displayed by the renderer. /// NOTE: The renderer is not repainted, to refresh the display call /// Repaint() after calling this. /// public int Slice { get { return (int)this.Metadata["CurrentSlice"]; } set { //this.m_SliceSlider_Scroll(this.m_SliceSlider, new ScrollEventArgs(ScrollEventType.SmallIncrement, this.Slice, value)); if (!this.m_HasRenderedFirstTime) return; // Set the slice value this.m_SliceSlider.Value = value; int slice = value; // Update the slice this.Metadata["CurrentSlice"] = slice; for (int i = 0; i < this.Inputs.Count; i++) { this.ConvertScalarToSlice(i); this.ConvertSliceToBitmap(i); } } } //===================================================================== #endregion #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). /// public double ZoomFactor { get { return (double)this.Metadata["ZoomFactor"]; } set { this.Metadata["ZoomFactor"] = value; } } //===================================================================== #endregion #region PaintBorder //===================================================================== /// /// Gets/sets if a border is drawn around the slice. /// public bool PaintBorder { get { return (bool)this.Metadata["PaintBorder"]; } set { this.Metadata["PaintBorder"] = 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 ArrayIndex //===================================================================== /// /// Gets/sets the index to view for itkPixels with an array type. /// This property has no effect for Scalar images. /// protected uint ArrayIndex { get { return (uint)this.Metadata["ArrayIndex"]; } set { this.Metadata["ArrayIndex"] = value; } } //===================================================================== #endregion #region InputsAsScalar //===================================================================== private List m_InputsAsScalar = new List(); /// /// Gets the list of Input objects as scalar (not array) images. /// public List InputsAsScalar { get { return this.m_InputsAsScalar; } } //===================================================================== #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 #region InputsAsRescaled //===================================================================== private List m_InputsAsRescaled = new List(); /// /// Gets the list of Input objects as a rescaled UC image. /// public List InputsAsRescaled { get { return this.m_InputsAsRescaled; } } //===================================================================== #endregion #region InputsAsBitmap //===================================================================== private List m_InputsAsBitmap = new List(); /// /// Gets the list of image slices as renderable bitmaps. /// protected List InputsAsBitmap { get { return this.m_InputsAsBitmap; } } //===================================================================== #endregion //===================================================================== #endregion #region Events //===================================================================== #region SliceChanged //===================================================================== /// /// EventArg delegate for allowing listeners access to the /// RendererBase which rasied the event. /// /// public delegate void SliceChangedHandler(siGdiSliceRenderer 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. /// /// This method is thread-safe. public override void Initialise() { // Call the base base.Initialise(); // Make the call thread-safe if (this == null || this.Form == null) { return; } else if (this.Form.InvokeRequired) { this.Form.Invoke(new VoidMethodCallback(this.Initialise)); return; } // Initialise form this.Form.Initialise(this); // Ensure the default slice is set this.InitialiseInputs(); // Get the input and inputAsSlice const int indexInput = 0; itk.itkImageBase input = this.InputAsImage(indexInput); itk.itkImageBase inputAsSlice = this.InputsAsSlice[indexInput]; // Compute the size of the information string SizeF sizeInfoString = this.GetFormGraphics().MeasureString(input.ToString(), s_ImageInfoFont); sizeInfoString.Height += 2.0F; // 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 + 14), (int)Math.Ceiling(scaledSize[1] + 2*SPACE_AROUND_IMAGE + sizeInfoString.Height)); // 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; } /// /// Initialises the slice to renderable. /// /// This method is thread-safe. public void InitialiseInputsAsSlice() { // Make the call thread-safe if (this == null || this.Form == null) { return; } else if (this.Form.InvokeRequired) { this.Form.Invoke(new VoidMethodCallback(this.InitialiseInputsAsSlice)); return; } // Ensure the default slice is set if (!this.ContainsMetadata("CurrentSlice")) this.Metadata["CurrentSlice"] = 0; // Update the slice to render for (int i = 0; i < this.Inputs.Count; i++) this.ConvertSliceToBitmap(i); } /// /// Initialises the image to slice to renderable. /// /// This method is thread-safe. public void InitialiseInputs() { // Make the call thread-safe if (this == null || this.Form == null) { return; } else if (this.Form.InvokeRequired) { this.Form.Invoke(new VoidMethodCallback(this.InitialiseInputs)); return; } // Ensure the default slice is set if (!this.ContainsMetadata("CurrentSlice")) this.Metadata["CurrentSlice"] = 0; // Convert the input to a renderable bitmap for (int i = 0; i < this.Inputs.Count; i++) this.ConvertInputToBitmap(i); } /// /// Forces the Renderer to repaint. /// /// This method is thread-safe. public override void Repaint() { // Make the call thread-safe if (this == null || this.Form == null) { return; } else if (this.Form.InvokeRequired) { this.Form.Invoke(new VoidMethodCallback(this.Repaint)); return; } // Force the form to repaint if (this.Visible) this.Form.Repaint(); } /// /// Close the RendererBase and dispose of all resources. /// /// This method is thread-safe. public override void Close() { // Make the call thread-safe if (this == null || this.Form == null) { return; } else if (this.Form.InvokeRequired) { this.Form.Invoke(new VoidMethodCallback(this.Close)); return; } // Close and dispose base.Close(); this.Dispose(); } /// /// Adds the given image as a label overlayed on the other inputs. /// /// public void AddInputAsLabel(itk.itkImageBase label, siLookupTable lut) { int index = this.Inputs.Count; string keyLUT = String.Format("LookupTable{0}", index); label.Metadata[keyLUT] = lut; label.Metadata["IsLabel"] = true; this.Inputs.Add(label); this.InitialiseInputs(); } /// /// Returns a bitmap which represents a "screen capture". /// /// public override Bitmap GetScreenCapture() { // Get the image rectangle in screen coords RectangleF rectfImageScreen = (RectangleF)this.Metadata["ImageScreenRectangle"]; int x = (int)rectfImageScreen.X; int y = (int)rectfImageScreen.Y; int width = (int)rectfImageScreen.Width; int height = (int)rectfImageScreen.Height; Rectangle rectScreen = new Rectangle(0, 0, width + x, height + y); // Create the bitmap to render to Bitmap bitmapScreen = new Bitmap(rectScreen.Width, rectScreen.Height); Graphics gScreen = Graphics.FromImage(bitmapScreen); gScreen.Clear(Color.Black); // Paint to the bitmap bool oldPaintBorder = this.PaintBorder; this.PaintBorder = false; this.OnPaint(new PaintEventArgs(gScreen, rectScreen)); gScreen.Dispose(); this.PaintBorder = oldPaintBorder; // Crop the screen image Rectangle rectCrop = new Rectangle(0, 0, width, height); Bitmap bitmapCrop = new Bitmap(width, height); Graphics gCrop = Graphics.FromImage(bitmapCrop); gCrop.DrawImageUnscaled(bitmapScreen, -x, -y); gCrop.Dispose(); bitmapScreen.Dispose(); // Return return bitmapCrop; } //===================================================================== #endregion #region Paint Methods //===================================================================== protected override void OnPaint(PaintEventArgs e) { // Get the graphics Graphics g = e.Graphics; // Get the first input for various information purposes itk.itkImageBase input = this.InputAsImage(0); // Set the form title double zoomAsPercent = this.ZoomFactor*100.0; this.TitleText = String.Format("{0} [{1}%]", Path.GetFileName(input.Name), zoomAsPercent.ToString()); // Force nearest-neighbour interpolation g.InterpolationMode = InterpolationMode.NearestNeighbor; // Draw the image information in the top left hand corner. // NOTE: It is drawn first so that the image will overwrite it, // if the form is resized as such. g.DrawString(input.ToString(), s_ImageInfoFont, s_ImageInfoBrush, (float)SPACE_AROUND_IMAGE, 0F); SizeF sizeInfoString = g.MeasureString(input.ToString(), new Font("Arial", 9.0F)); sizeInfoString.Height += 2.0F; // Draw each input for (int indexInput = 0; indexInput < this.Inputs.Count; indexInput++) this.DrawInput(indexInput, g, sizeInfoString.Height); // Set we have rendered this.m_HasRenderedFirstTime = true; // Give the base class a chance to paint base.OnPaint(e); } /// ///// Draws the given input using the PaintEventArgs e. /// /// The index of the input to draw. /// The Graphics object for rendering. /// The height the size info in the top-left corner. private void DrawInput(int indexInput, Graphics g, float sizeInfoHeight) { // Get the input image data itk.itkImageBase inputAsSlice = this.InputsAsSlice[indexInput]; Bitmap inputAsBitmap = this.InputsAsBitmap[indexInput]; if (inputAsSlice == null) return; if (inputAsBitmap == null) return; // Draw the scaled bitmap to the graphics handle double[] scaledSize = new double[inputAsSlice.Dimension]; scaledSize[0] = this.ZoomFactor * (double)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]); // Adjust offset for zoom factor PointF offset = this.Offset; offset.X *= (float)this.ZoomFactor; offset.Y *= (float)this.ZoomFactor; // Compute image rectangle double scaledX = Math.Ceiling(((double)this.ViewportSize.Width / 2.0) - (scaledSize[0] / 2.0) + offset.X); double scaledY = Math.Ceiling(((double)this.ViewportSize.Height / 2.0) - (scaledSize[1] / 2.0) + (sizeInfoHeight / 2.0) + offset.Y); double scaledWidth = Math.Floor(scaledSize[0]); double scaledHeight = Math.Floor(scaledSize[1]); RectangleF rectDest = new RectangleF((float)(scaledX), (float)(scaledY), (float)(scaledWidth), (float)(scaledHeight)); RectangleF rectSource = new RectangleF((float)(0.0), (float)(0.0), (float)(inputAsSlice.Size[0]), (float)(inputAsSlice.Size[1])); // Adjust for index being in center of pixel if (this.ZoomFactor > 1.0) rectSource.Offset(-0.5F, -0.5F); // Draw transparency squares (if required) if (inputAsBitmap.PixelFormat == PixelFormat.Format32bppArgb) { // Draw a background indicating transparency int width = inputAsBitmap.Width; int height = inputAsBitmap.Height; Bitmap back = siColorHelper.CreateTransparentBackground(width, height); g.DrawImage(back, rectDest, rectSource, GraphicsUnit.Pixel); } // Draw the image g.DrawImage(inputAsBitmap, rectDest, rectSource, GraphicsUnit.Pixel); // Do some "first-input-only" processing if (indexInput == 0) { // Draw a border around the image if (this.PaintBorder) { Pen pen = new Pen(s_ImageBorderColor, 1.0F); g.DrawRectangle(pen, (float)scaledX - 1.0F, (float)scaledY - 1.0F, (float)scaledWidth, (float)scaledHeight); } // Save the rectangle this.Metadata["ImageScreenRectangle"] = rectDest; } } //===================================================================== #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; // 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 itk.itkIndex index = this.TransformScreenPointToImageIndex(e.Location); // 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 { // The location is inside the image // Convert the screen mouse location to image physical location // NOTE: This value does NOT take into account the image direction cosines itk.itkPoint point = this.TransformScreenPointToImagePoint(e.Location); // Update the Metadata this.Metadata["LastImagePointMouseMove"] = point; this.Metadata["LastImageIndexMouseMove"] = index; bool isMouseInsideImageSpace = this.InputAsImage(0).LargestPossibleRegion.IsInside(index); this.Metadata["IsMouseInsideImageSpace"] = isMouseInsideImageSpace; if (isMouseInsideImageSpace) this.Metadata["LastImagePixelMouseMove"] = this.InputAsImage(0).GetPixel(index); else this.Metadata["LastImagePixelMouseMove"] = null; if (isMouseInsideImageSpace) newCursor = Cursors.Cross; else 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) { base.OnMouseWheel(e); if (e.Delta > 0) this.ZoomOutByFactorOfTwo(); else this.ZoomInByFactorOfTwo(); this.Repaint(); } protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); // Zoom in or out if (e.KeyCode == Keys.Add || e.KeyCode == Keys.Oemplus) this.ZoomInByFactorOfTwo(); else if (e.KeyCode == Keys.Subtract || e.KeyCode == Keys.OemMinus) this.ZoomOutByFactorOfTwo(); // Array index else if (e.KeyCode == Keys.NumPad0 || e.KeyCode == Keys.D0) this.SetArrayIndex(0U); else if (e.KeyCode == Keys.NumPad1 || e.KeyCode == Keys.D1) this.SetArrayIndex(1U); else if (e.KeyCode == Keys.NumPad2 || e.KeyCode == Keys.D2) this.SetArrayIndex(2U); else if (e.KeyCode == Keys.NumPad3 || e.KeyCode == Keys.D3) this.SetArrayIndex(3U); else if (e.KeyCode == Keys.NumPad4 || e.KeyCode == Keys.D4) this.SetArrayIndex(4U); else if (e.KeyCode == Keys.NumPad5 || e.KeyCode == Keys.D5) this.SetArrayIndex(5U); else if (e.KeyCode == Keys.NumPad6 || e.KeyCode == Keys.D6) this.SetArrayIndex(6U); else if (e.KeyCode == Keys.NumPad7 || e.KeyCode == Keys.D7) this.SetArrayIndex(7U); else if (e.KeyCode == Keys.NumPad8 || e.KeyCode == Keys.D8) this.SetArrayIndex(8U); else if (e.KeyCode == Keys.NumPad9 || e.KeyCode == Keys.D9) this.SetArrayIndex(9U); // Force a repaint this.Repaint(); } private void SetArrayIndex(uint index) { if (this.Inputs[0] is itk.itkImageBase) { itk.itkImageBase input = this.Inputs[0] as itk.itkImageBase; if (input.PixelType.ArrayAsEnum == itk.itkPixelArrayEnum.ArrayVector || input.PixelType.ArrayAsEnum == itk.itkPixelArrayEnum.ArrayCovariantVector || input.PixelType.ArrayAsEnum == itk.itkPixelArrayEnum.ArrayVariableLengthVector || input.PixelType.ArrayAsEnum == itk.itkPixelArrayEnum.ArrayVectorImage) { if (index >= 0 && index < input.Dimension) { this.ArrayIndex = index; this.InitialiseInputs(); } } } } void GdiSliceRenderer_SliceChanged(siGdiSliceRenderer 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 (e.Type == ScrollEventType.EndScroll) return; if (!this.m_HasRenderedFirstTime) return; // Set the slice int slice = e.NewValue; // Update the slice this.Metadata["CurrentSlice"] = slice; for (int i = 0; i < this.Inputs.Count; i++) { this.ConvertScalarToSlice(i); this.ConvertSliceToBitmap(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, e.NewValue); 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.itkImageBase. /// 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 NotSupportedException("The " + this.TypeName + " does not support the type of Input[" + index.ToString() + "]."); } /// /// This method converts the given input index object into a bitmap /// For example, if the input is 3D this method will extract the /// appropriate slice. Also, this method will convert the input to a /// GDI+ natively supported type (ie. System::Byte). /// /// private void ConvertInputToBitmap(int index) { // Create the list of scalar images while (this.InputsAsScalar.Count < this.Inputs.Count) this.InputsAsScalar.Add(null); // Create the list of image slices while (this.InputsAsSlice.Count < this.Inputs.Count) this.InputsAsSlice.Add(null); // Create the list of rescaled UC images while (this.InputsAsRescaled.Count < this.Inputs.Count) this.InputsAsRescaled.Add(null); // Create the list of bitmaps while (this.InputsAsBitmap.Count < this.Inputs.Count) this.InputsAsBitmap.Add(null); // Update to renderable bitmap this.ConvertInputToScalar(index); this.ConvertScalarToSlice(index); this.ConvertSliceToBitmap(index); // Reset the requested region for the input itk.itkImageBase input = this.InputAsImage(index); input.RequestedRegion = input.LargestPossibleRegion; } private void ConvertInputToScalar(int index) { try { // Get the input and scalar images itk.itkImageBase input = this.InputAsImage(index); itk.itkImageBase scalar = this.InputsAsScalar[index]; if (input.PixelType.IsScalar || input.PixelType.IsColor) { scalar = input; } else { // Create the new scalar image scalar = itk.itkImage.New(new itk.itkPixelType(input.PixelType.TypeAsEnum), input.Dimension); // Extract the index itk.itkVectorIndexSelectionCastImageFilter filterSelect = itk.itkVectorIndexSelectionCastImageFilter.New(input, scalar); filterSelect.RemoveAllObservers(); filterSelect.SetInput(input); filterSelect.Index = this.ArrayIndex; filterSelect.Update(); filterSelect.GetOutput(scalar); input.DisconnectPipeline(); scalar.DisconnectPipeline(); filterSelect.Dispose(); } // Set the scalar this.InputsAsScalar[index] = scalar; } catch (Exception ex) { Trace.WriteLine("ERROR: Could not convert from input image to scalar image:"); Trace.WriteLine(ex.ToString()); } } private void ConvertScalarToSlice(int index) { try { // Get the scalar and slice images itk.itkImageBase scalar = this.InputsAsScalar[index]; itk.itkImageBase slice = this.InputsAsSlice[index]; if (scalar.Dimension == 2) { slice = scalar; } else if (scalar.Dimension == 3) { // Dispose of the old slice if (slice != null && !slice.IsDisposed) { slice.Dispose(); slice = null; } // Create the new slice image slice = itk.itkImage.New(scalar.PixelType, 2); // Extract the slice itk.itkExtractImageFilter filterExtract = itk.itkExtractImageFilter.New(scalar, slice); filterExtract.RemoveAllObservers(); filterExtract.SetInput(scalar); filterExtract.ExtractSlice(2, this.Slice); filterExtract.Update(); filterExtract.GetOutput(slice); slice.DisconnectPipeline(); filterExtract.Dispose(); } // Set the slice this.InputsAsSlice[index] = slice; } catch (Exception ex) { Trace.WriteLine("ERROR: Could not convert from scalar image to slice image:"); Trace.WriteLine(ex.ToString()); } } private void ConvertSliceToBitmap(int index) { try { // Get the input and slice as images itk.itkImageBase input = this.InputAsImage(index); itk.itkImageBase scalar = this.InputsAsScalar[index]; itk.itkImageBase slice = this.InputsAsSlice[index]; itk.itkImageBase rescaled = this.InputsAsRescaled[index]; Bitmap bitmap = this.InputsAsBitmap[index]; // Dispose of the old bitmap if (bitmap != null) { bitmap.Dispose(); bitmap = null; } // Handle the slice pixel type if (slice.PixelType.TypeAsEnum != itk.itkPixelTypeEnum.UnsignedChar) { double minValue = 0.0; double maxValue = 255.0; // Get the min/max from either slice or compute from scalar if (slice.Metadata.ContainsKey("MinimumValueAsD") && slice.Metadata.ContainsKey("MaximumValueAsD")) { // Get minimum and maximum values from slice minValue = (double)slice.Metadata["MinimumValueAsD"]; maxValue = (double)slice.Metadata["MaximumValueAsD"]; } else { // Check if we need to compute the min/max from scalar if (!scalar.Metadata.ContainsKey("MinimumValueAsD") && !scalar.Metadata.ContainsKey("MaximumValueAsD")) { // Compute from scalar itk.itkMinimumMaximumImageCalculator calcMinMax = itk.itkMinimumMaximumImageCalculator.New(scalar); calcMinMax.SetImage(scalar); calcMinMax.Compute(); scalar.Metadata.Add("MinimumValue", calcMinMax.Minimum); scalar.Metadata.Add("MinimumValueAsD", calcMinMax.Minimum.ValueAsD); scalar.Metadata.Add("MaximumValue", calcMinMax.Maximum); scalar.Metadata.Add("MaximumValueAsD", calcMinMax.Maximum.ValueAsD); scalar.DisconnectPipeline(); calcMinMax.Dispose(); } // Get minimum and maximum values from the scalar minValue = (double)scalar.Metadata["MinimumValueAsD"]; maxValue = (double)scalar.Metadata["MaximumValueAsD"]; } // Rescale the slice for 8-bit display rescaled = itk.itkImage.New(itk.itkPixelType.UC, 2); itk.itkShiftScaleImageFilter filterShiftScale = itk.itkShiftScaleImageFilter.New(slice, rescaled); filterShiftScale.RemoveAllObservers(); filterShiftScale.SetInput(slice); filterShiftScale.Shift = -1 * minValue; filterShiftScale.Scale = 1.0 / ((maxValue - minValue) / (itk.itkPixel_UC.NewMax().ValueAsD + 1.0)); filterShiftScale.Update(); filterShiftScale.GetOutput(rescaled); slice.DisconnectPipeline(); rescaled.DisconnectPipeline(); filterShiftScale.Dispose(); } else { rescaled = slice; } // Check the metadata for palette and format string keyLookupTable = "LookupTable" + index.ToString(); int numComponents = 1; ColorPalette palette = null; PixelFormat format; if (input.PixelType.ArrayAsEnum == itk.itkPixelArrayEnum.ArrayRGB) { format = PixelFormat.Format24bppRgb; numComponents = 3; } else if (input.PixelType.ArrayAsEnum == itk.itkPixelArrayEnum.ArrayRGBA) { format = PixelFormat.Format32bppArgb; numComponents = 4; } else if (input.Metadata.ContainsKey(keyLookupTable) && input.Metadata[keyLookupTable] is siGdiLookupTable) { // Get LUT siGdiLookupTable lut = input.Metadata[keyLookupTable] as siGdiLookupTable; // Use the palette and format palette = lut.GetGdiPalette(); format = lut.GetGdiPixelFormat(); } else { // Use the default palette and format siGdiLookupTable lut = new siGdiLookupTable(); lut.SetTableRange(rescaled.PixelType); lut.SetTableToGreyscale(); //lut.SetTableRange(0.0, 255.0, 32); //lut.SetTableToHueRange(0.0, 0.16666666); //lut.SetTableToGreyscale(); palette = lut.GetGdiPalette(); format = lut.GetGdiPixelFormat(); } // Check if the stride is the same as the width if (numComponents == 1 && rescaled.Size[0] % 4 == 0) { // Width = Stride AND scalar image: simply use the Bitmap constructor bitmap = new Bitmap(rescaled.Size[0], // Width rescaled.Size[1], // Height rescaled.Size[0], // Stride format, // PixelFormat rescaled.Buffer // Buffer ); } else { unsafe { // Width != stride AND/OR RGB/A image: copy data from buffer to bitmap int width = rescaled.Size[0]; int height = rescaled.Size[1]; byte* buffer = (byte*)rescaled.Buffer.ToPointer(); // Compute the stride int stride = width; if (rescaled.Size[0] % 4 != 0) stride = ((rescaled.Size[0] / 4) * 4 + 4); bitmap = new Bitmap(width, height, format); Rectangle rect = new Rectangle(0, 0, width, height); BitmapData bitmapData = bitmap.LockBits(rect, ImageLockMode.WriteOnly, format); for (int k = 0; k < height; k++) // Row { byte* row = (byte*)bitmapData.Scan0 + (k * stride * numComponents); for (int j = 0; j < width; j++) // Column { for (int i = 0; i < numComponents; i++) // Component { int swappedi = this.SwapArrayComponent(input.PixelType.ArrayAsEnum, numComponents, i); row[(j * numComponents) + i] = buffer[(k * width * numComponents) + (j * numComponents) + swappedi]; } } } bitmap.UnlockBits(bitmapData); } } // Finish setting up the renderable bitmap if (palette != null) bitmap.Palette = palette; this.InputsAsRescaled[index] = rescaled; this.InputsAsBitmap[index] = bitmap; } catch (Exception ex) { Trace.WriteLine("ERROR: Could not convert from slice image to bitmap:"); Trace.WriteLine(ex.ToString()); } } /// /// Swaps the array component order for the GDI+ library. /// /// /// /// /// private int SwapArrayComponent(itk.itkPixelArrayEnum enumArray, int numComponents, int component) { switch (enumArray) { case itk.itkPixelArrayEnum.ArrayRGBA: switch (component) { case 0: return 2; case 1: return 1; case 2: return 0; case 3: return 3; default: return component; } default: return (numComponents - component - 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 //===================================================================== /// /// Transform the given discrete point in screen space to physical image space. /// /// /// public itk.itkPoint TransformScreenPointToImagePoint(Point screen) { // 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), out point); return point; } /// /// Transform the given discrete point in screen space to discrete pixel space. /// /// /// public itk.itkIndex TransformScreenPointToImageIndex(Point screen) { // Convert the screen location to an image index RectangleF rectImageScreen = (RectangleF)this.Metadata["ImageScreenRectangle"]; 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]); // Set slice for 3-D images if (cindex.Dimension == 3) cindex[2] = (double)((int)this.Metadata["CurrentSlice"]); // Adjust for index being in center of pixel if (this.ZoomFactor > 1.0) { cindex[0] -= 0.5; cindex[1] -= 0.5; } // Return return cindex.ToIndex(); } /// /// Transform a continuous index in pixel space to a continuous point in screen space. /// This method assumes the upper-left corner of a pixel is the location. /// /// The continuous index to convert. /// public PointF TransformImageContinuousIndexToScreenPoint(itk.itkContinuousIndex cindex) { return this.TransformImageContinuousIndexToScreenPoint(cindex, false); } /// /// Transform a continuous index in pixel space to a continuous point in screen space. /// /// The continuous index to convert. /// If true, the location is adjusted to make the center of pixel the location. /// public PointF TransformImageContinuousIndexToScreenPoint(itk.itkContinuousIndex cindex, bool makeCenterOfPixel) { // Convert the screen location to an image index RectangleF rectImageScreen = (RectangleF)this.Metadata["ImageScreenRectangle"]; itk.itkImageBase input = this.InputAsImage(0); PointF screenPoint = new PointF(); // Make the point the center of the pixel if (makeCenterOfPixel) for (int i = 0; i < cindex.Dimension; i++) cindex[i] += 0.5; // Convert from cidnex to screenPoint 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; } /// /// Transform a discrete index in pixel space to a continuous point in screen space. /// This method assumes the upper-left corner of a pixel is the location. /// /// The index to convert. /// public PointF TransformImageIndexToScreenPoint(itk.itkIndex index) { return this.TransformImageIndexToScreenPoint(index, false); } /// /// Transform a discrete index in pixel space to a continuous point in screen space. /// /// The index to convert. /// If true, the location is adjusted to make the center of pixel the location. /// public PointF TransformImageIndexToScreenPoint(itk.itkIndex index, bool makeCenterOfPixel) { itk.itkContinuousIndex cindex = new itk.itkContinuousIndex(index); return this.TransformImageContinuousIndexToScreenPoint(cindex, makeCenterOfPixel); } /// /// Transform a continuous point in physical space to a continuous point in screen space. /// /// /// The point to convert. /// public PointF TransformImagePointToScreenPoint(itk.itkImageBase image, itk.itkPoint point) { return this.TransformImagePointToScreenPoint(image, point, false); } /// /// Transform a continuous point in physical space to a continuous point in screen space. /// /// /// The point to convert. /// If true, the location is adjusted to make the center of pixel the location. /// public PointF TransformImagePointToScreenPoint(itk.itkImageBase image, itk.itkPoint point, bool makeCenterOfPixel) { itk.itkContinuousIndex cindex; image.TransformPhysicalPointToContinuousIndex(point, out cindex); return this.TransformImageContinuousIndexToScreenPoint(cindex, makeCenterOfPixel); } //===================================================================== #endregion } //========================================================================= #endregion }