Click here to Skip to main content
15,891,567 members
Articles / Programming Languages / Visual Basic

A custom control for image annotations and image processing

Rate me:
Please Sign up or sign in to vote.
4.50/5 (8 votes)
18 May 20045 min read 142.7K   3.2K   57  
Evolution of an image annotation control after its use for image processing.
Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Windows
Imports ClassesYVDH.Annotations

Public Class AnnotatedImage
  'Class that permits rendering of an image and overlayed annotations
  'It will size itself and parent controls must implement the Resize event
  'to adjust their size accordingly ...
  'No IO is provided, just set the internal bitmap.
  'This class does not do any computations or anything, it just does the drawing.
  Inherits System.Windows.Forms.Control

  'Class variables, mirrored by a property
  Private objBitmap As Bitmap, oDefaultCursor As Cursor
  Private objMaximimumSize As New Size(800, 600), iWindowSize As Integer
  Private objMousePos As New Point(0, 0), bDirty As Boolean, bBusy As Boolean
  Private sDisplayZoom As Single, ptDisplayOffset As Point
  Private WithEvents objAnnotations As Annotations

  'Public events
  Public Event DisplayZoomChanged(ByVal sDisplayZoom As Single)
  Public Event PixelAveragingWindowSizeChanged(ByVal iWindowSize As Integer)
  Public Event BitmapChanged(ByVal objBitmap As Bitmap)

  'Public events handled in the Annotations class, and passed on too!
  Public Event AnnotationCreationDone(ByVal objBitmap As Bitmap, ByVal objAnnotation As Annotation)
  Public Event AnnotationCreationCanceled(ByVal objAnnotation As Annotation)
  Public Event AnnotationCreationPointAdded(ByVal objAnnotation As Annotation)
  Public Event AnnotationCreationPointRemoved(ByVal objAnnotation As Annotation)
  Public Event AnnotationCreationMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As Annotation)
  Public Event AnnotationClick(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As Annotation)
  Public Event AnnotationHover(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As Annotation)

#Region " Component Designer generated code "

  Public Sub New()
    MyBase.New()

    objAnnotations = New Annotations(Me)

    ' This call is required by the Component Designer.
    InitializeComponent()

    setstyle(ControlStyles.UserPaint, True)
    setstyle(ControlStyles.AllPaintingInWmPaint, True)
    setstyle(ControlStyles.DoubleBuffer, True)
  End Sub

  'Control overrides dispose to clean up the component list.
  Protected Overloads Overrides Sub Dispose(ByVal disposing As Boolean)
    If disposing Then
      If Not (components Is Nothing) Then
        components.Dispose()
      End If
    End If
    MyBase.Dispose(disposing)
  End Sub

  Protected Overrides Sub Finalize()
    Me.Bitmap = Nothing
  End Sub

  'Required by the Control Designer
  Private components As System.ComponentModel.IContainer

  ' NOTE: The following procedure is required by the Component Designer
  ' It can be modified using the Component Designer.  Do not modify it
  ' using the code editor.
  <System.Diagnostics.DebuggerStepThrough()> Private Sub InitializeComponent()
    '
    'AnnotatedImage
    '

  End Sub
#End Region

#Region "Properties"

  Public Property Busy() As Boolean
    Get
      Return bBusy
    End Get
    Set(ByVal Value As Boolean)
      bBusy = Value
      If bBusy Then
        Cursor = System.Windows.Forms.Cursors.WaitCursor
      Else
        Cursor = System.Windows.Forms.Cursors.Arrow
      End If
    End Set
  End Property

  Public ReadOnly Property ImageSize() As Size
    Get
      Return New Size(Me.Bitmap.Width, Me.Bitmap.Height)
    End Get
  End Property

  Public ReadOnly Property DisplaySize() As Size
    Get
      Dim objBitmap As Bitmap = Me.Bitmap
      If Not objBitmap Is Nothing Then
        Dim sZoom As Single = Me.DisplayZoom
        Return New Size(objBitmap.Width * sZoom, objBitmap.Height * sZoom)
      Else
        Return New Size(0, 0)
      End If
    End Get
  End Property

  Public ReadOnly Property Annotations() As Annotations
    Get
      Return Me.objAnnotations
    End Get
  End Property

  Public Property MaximimumSize() As Size
    Get
      Return Me.objMaximimumSize
    End Get
    Set(ByVal Value As Size)
      Me.objMaximimumSize = Value
    End Set
  End Property

  Public Property DisplayOffset() As Point
    Get
      Return Me.ptDisplayOffset
    End Get
    Set(ByVal Value As Point)
      Debug.WriteLine("Set display offset at " & Value.ToString(), "AnnotatedImage.DisplayOffset")
      If Value.X < 0 Then Value.X = 0
      If Value.Y < 0 Then Value.Y = 0
      Dim iDeltaX As Integer = CInt((DisplaySize.Width - Me.Width) / Me.DisplayZoom)
      Dim iDeltaY As Integer = CInt((DisplaySize.Height - Me.Height) / Me.DisplayZoom)

      If Value.X > iDeltaX Then Value.X = iDeltaX
      If Value.Y > iDeltaY Then Value.Y = iDeltaY
      Me.ptDisplayOffset = Value
      Me.Refresh()
    End Set
  End Property

  Public Property DisplayZoom() As Single
    Get
      Return Me.sDisplayZoom
    End Get
    Set(ByVal Value As Single)
      If Math.Abs(Value - 1.0) < 0.001 Then Value = 1.0
      Dim objOffsetScale As Single = Value / Me.sDisplayZoom
      Me.sDisplayZoom = Value
      Debug.WriteLine("Set display zoom at " & Value, "AnnotatedImage.DisplayZoom")
      SizeControl()

      'We have to check if we have to reset or scale the display offset
      'It is actually setting the offset that triggers the redraw!
      If DisplayImageBiggerThanCanvas() = False Then
        Me.DisplayOffset = New Point(0, 0)
      Else
        Dim objOffset As Point = Me.DisplayOffset
        objOffset.X *= objOffsetScale
        objOffset.Y *= objOffsetScale
        Me.DisplayOffset = objOffset
      End If

      RaiseEvent DisplayZoomChanged(Value)
    End Set
  End Property

  Public Property Bitmap() As Bitmap
    Get
      Return Me.objBitmap
    End Get

    Set(ByVal Value As Bitmap)
      Me.objBitmap = Value

      'Clear all annotations
      Me.Annotations.AnnotationContainers.Clear()

      'Causes redraw
      DisplayZoom = ComputeInitialDisplayZoom()

      RaiseEvent BitmapChanged(Value)
    End Set
  End Property

  Public Property PixelAveragingWindowSize() As Integer
    Get
      Return Me.iWindowSize
    End Get
    Set(ByVal Value As Integer)
      Me.iWindowSize = Value
      RaiseEvent PixelAveragingWindowSizeChanged(Value)
    End Set
  End Property

  Private Property Dirty() As Boolean
    'This indicates if XOR drawing has occured since last refresh, which is necessary for 
    'the routines drawing XOR (erasable) rectangles and lines...
    Get
      Return Me.bDirty
    End Get
    Set(ByVal Value As Boolean)
      Me.bDirty = Value
    End Set
  End Property

#End Region

#Region "Public methods"
  Public Sub DrawErasableRectangle(ByVal objRect As Rectangle)
    'Draw an eraseble rectangle.
    'Input is in client coordinates, static old rectangle is in screen coordinates!
    'Checks that we do not draw outside client area
    Static objOldRect As Rectangle

    'Take a middle of the road color
    Dim objColor As New Color
    objColor = Color.Gray

    'Only erase if required, e.g. not when control has been refreshed ...
    If Me.Dirty = True And objOldRect.Width > 0 Then
      ControlPaint.DrawReversibleFrame(objOldRect, objColor, FrameStyle.Dashed)
    End If

    'Make sure we are not outside the client area
    objRect.Intersect(New Rectangle(0, 0, Me.Width, Me.Height))

    objOldRect = Me.RectangleToScreen(objRect)
    ControlPaint.DrawReversibleFrame(objOldRect, objColor, FrameStyle.Dashed)
    Me.Dirty = True
  End Sub

  Public Sub DrawErasableLine(ByVal objPt1 As Point, ByVal objPt2 As Point)
    'Draw an eraseble line.
    'Pts are in client coordinates
    'static old Pts are in screen coordinates!
    Static objOldPt1 As Point
    Static objOldPt2 As Point

    'Take a middle of the road color
    Dim objColor As New Color, objTempPt2 As Point
    objColor = Color.Gray

    'Avoid trying to erase a rectangle if canvas has just been redrawn!
    If Me.Dirty Then
      ControlPaint.DrawReversibleLine(objOldPt1, objOldPt2, objColor)
    End If

    'Convert to screen coordinates
    objOldPt1 = Me.PointToScreen(objPt1)
    objOldPt2 = Me.PointToScreen(objPt2)
    ControlPaint.DrawReversibleLine(objOldPt1, objOldPt2, objColor)

    Me.Dirty = True
  End Sub

  Public Overloads Sub SaveImage(ByVal strFileName As String, ByVal imgFormat As System.Drawing.Imaging.ImageFormat)
    Me.Bitmap.Save(strFileName, imgFormat)
  End Sub

  Public Overloads Function LoadImage(ByVal strFileName As String) As Boolean
    ' Sets up an image object to be displayed.
    Dim objBitmap As Bitmap = Bitmap.FromFile(strFileName), bSucces As Boolean = False

    If objBitmap.PixelFormat = Drawing.Imaging.PixelFormat.Format24bppRgb Then
      'Create an in-memory copy, and assign to custom control
      Dim objMemoryBitmap As New Bitmap(objBitmap.Width, objBitmap.Height, System.Drawing.Imaging.PixelFormat.Format24bppRgb)
      Dim g As Graphics = Graphics.FromImage(objMemoryBitmap)
      g.DrawImage(objBitmap, New Rectangle(0, 0, objBitmap.Width, objBitmap.Height))
      objMemoryBitmap.SetResolution(objBitmap.HorizontalResolution, objBitmap.VerticalResolution)
      Me.Bitmap = objMemoryBitmap
      bSucces = True
    Else
      MessageBox.Show("Error loading image: only 24-bit images are supported!", "sRGB Tool", MessageBoxButtons.OK, MessageBoxIcon.Error)
    End If

    'Dispose of original bitmap
    objBitmap.Dispose()

    'return if successful
    Return bSucces
  End Function
#End Region

#Region "Private methods"

  Private Function DisplayImageBiggerThanCanvas() As Boolean
    If Me.MaximimumSize.Width < Me.DisplaySize.Width Or Me.MaximimumSize.Height < Me.DisplaySize.Height Then
      Return True
    Else
      Return False
    End If
  End Function

  Private Function ComputeInitialDisplayZoom() As Single
    'Compute the best startDisplayZoom. Alos set the scrollbars to 0 
    If Not Me.Bitmap Is Nothing Then
      Dim sHorDisplayZoom As Single = MaximimumSize.Height / CSng(objBitmap.Height())
      Dim sVertDisplayZoom As Single = MaximimumSize.Width / CSng(objBitmap.Width())
      Return Math.Min(1, Math.Min(sHorDisplayZoom, sVertDisplayZoom))
    Else
      Return 1
    End If
  End Function

  Private Sub SizeControl()
    'We take care of sizing ourselves, based on the DisplayZoom
    'We cannot exceed the screensize MaximimumSize
    'Setting size in one call causes less events in parent!
    Dim objBitmap = Me.Bitmap, objSize As New Size(800, 600)
    If Not objBitmap Is Nothing Then
      'Compute size of control
      Dim iImageWidth As Integer = objBitmap.Width() * Me.DisplayZoom
      Dim iImageHeight As Integer = objBitmap.Height() * Me.DisplayZoom

      If iImageWidth > Me.MaximimumSize.Width Then
        objSize.Width = Me.MaximimumSize.Width
      Else
        objSize.Width = iImageWidth
      End If

      If iImageHeight > Me.MaximimumSize.Height Then
        objSize.Height = Me.MaximimumSize.Height
      Else
        objSize.Height = iImageHeight
      End If
    End If
    Debug.WriteLine("Control size set to " & objSize.ToString(), "AnnotatedImage.SizeControl")
    Me.Size = objSize
  End Sub

  Private Sub CheckAndAdjustBitmapRectangle(ByRef objRect As Rectangle)
    If objRect.X < 0 Then
      objRect.Width += objRect.X
      objRect.X = 0
    End If
    If objRect.Y < 0 Then
      objRect.Height += objRect.Y
      objRect.Y = 0
    End If

    Dim fZoom As Single = Me.DisplayZoom
    Dim objBitmap As Bitmap = Me.Bitmap
    If objRect.Bottom >= (objBitmap.Height * fZoom) Then
      objRect.Height = CInt(Math.Round((objBitmap.Height - objRect.Y - 1) * fZoom))
    End If

    If (objRect.Right >= (objBitmap.Width * fZoom)) Then
      objRect.Width = CInt(Math.Round((objBitmap.Width - objRect.X - 1) * fZoom))
    End If
  End Sub
#End Region

#Region "Form event handlers"
  Protected Overrides Sub OnPaint(ByVal pe As System.Windows.Forms.PaintEventArgs)
    'Add your custom paint code here
    If Not Me.Bitmap Is Nothing Then
      'Paint the bitmap onto the control, taking the clipractangle into account
      Dim sZoom As Single = Me.DisplayZoom
      Dim srcRect As Rectangle = New Rectangle(Me.DisplayOffset.X + pe.ClipRectangle.X / sZoom, _
                                               Me.DisplayOffset.Y + pe.ClipRectangle.Y / sZoom, _
                                               Math.Ceiling(pe.ClipRectangle.Width / Me.DisplayZoom), _
                                               Math.Ceiling(pe.ClipRectangle.Height / Me.DisplayZoom))
      Debug.WriteLine("Redrawing bitmap in rectangle (" _
                      & srcRect.Left & "," & srcRect.Top & ")-(" _
                      & srcRect.Right & "," & srcRect.Bottom & ")", "AnnotatedImage.Paint")
      pe.Graphics.DrawImage(Me.Bitmap, pe.ClipRectangle, srcRect, GraphicsUnit.Pixel)
    End If

    ' This trigers the Paint event so that annotations get drawn! 
    MyBase.OnPaint(pe)

    'Canvas is no longer dirty after full redraw
    Me.Dirty = False
  End Sub

  Private Sub AnnotatedImage_KeyDown(ByVal sender As Object, ByVal e As System.Windows.Forms.KeyEventArgs) Handles MyBase.KeyDown
    'Make sure we do not intercept modifiers that are in use by the annotations class!
    Debug.WriteLine("Pressed key " & e.KeyCode, "AnnotatedImage.KeyDown")
    If e.Shift Then
      Select Case e.KeyCode
        Case Keys.Z
          'Zoom in
          Me.DisplayZoom = Me.DisplayZoom * 1.25
        Case Keys.W
          'Decrease averaging window size
          If PixelAveragingWindowSize > 1 Then
            PixelAveragingWindowSize = PixelAveragingWindowSize - 2
          End If
      End Select
    ElseIf e.Alt Then
    ElseIf e.Control Then
    Else
      Select Case e.KeyCode
        Case Keys.Z
          'Zoom out
          Me.DisplayZoom = Me.DisplayZoom / 1.25
        Case Keys.W
          'Increase averaging window size
          PixelAveragingWindowSize = PixelAveragingWindowSize + 2
        Case Keys.F5
          'Cause refresh of client area
          Me.Refresh()
      End Select
    End If
  End Sub

  Private Sub AnnotatedImage_MouseDown(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseDown
    'One can always move the image. This is always with the middle mouse button
    Debug.WriteLine("Mouse button " & e.Button & " at " & e.X & "," & e.Y, "AnnotatedImage.MouseDown")
    If e.Button = MouseButtons.Right Then
      If DisplayImageBiggerThanCanvas() Then
        Me.Cursor = System.Windows.Forms.Cursors.SizeAll
        objMousePos.X = e.X
        objMousePos.Y = e.Y
      End If
    End If
  End Sub

  Private Sub AnnotatedImage_MouseWheel(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseWheel
    Debug.WriteLine("Mouse wheel Delta " & e.Delta, "AnnotatedImage.MouseWheel")
    If e.Delta > 0 Then
      Me.DisplayZoom = Me.DisplayZoom / 1.25
    Else
      Me.DisplayZoom = Me.DisplayZoom * 1.25
    End If
  End Sub

  Private Sub AnnotatedImage_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Forms.MouseEventArgs) Handles MyBase.MouseMove
    If Me.Bitmap Is Nothing Then Exit Sub

    If e.Button = MouseButtons.Right Then
      If DisplayImageBiggerThanCanvas() Then
        'Adjust DisplayOffset
        Dim objCurrentOffset As Point = Me.DisplayOffset
        Me.DisplayOffset = New Point(objCurrentOffset.X + (objMousePos.X - e.X) / Me.DisplayZoom, _
                                     objCurrentOffset.Y + (objMousePos.Y - e.Y) / Me.DisplayZoom)
        objMousePos.X = e.X
        objMousePos.Y = e.Y
      End If
    End If
  End Sub
#End Region

#Region "Annotations events"
  'These events are mostly passed on as is

  Private Sub objAnnotations_AnnotationClick(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationClick
    Debug.WriteLine("AnnotatedImage.AnnotationClick")
    RaiseEvent AnnotationClick(e, objAnnotation)
  End Sub

  Private Sub objAnnotations_AnnotationHover(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationHover
    RaiseEvent AnnotationHover(e, objAnnotation)
  End Sub

  'Annotation creation
  Private Sub objAnnotations_AnnotationCreationCanceled(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationCanceled
    Debug.WriteLine("AnnotatedImage.AnnotationCreationCanceled")
    RaiseEvent AnnotationCreationCanceled(objAnnotation)
  End Sub

  Private Sub objAnnotations_AnnotationCreationDone(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationDone
    Debug.WriteLine("AnnotatedImage.AnnotationCreationDone")
    RaiseEvent AnnotationCreationDone(Me.Bitmap, objAnnotation)
  End Sub

  Private Sub objAnnotations_AnnotationCreationMouseMove(ByVal e As System.Windows.Forms.MouseEventArgs, ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationMouseMove
    RaiseEvent AnnotationCreationMouseMove(e, objAnnotation)
  End Sub

  Private Sub objAnnotations_AnnotationCreationPointAdded(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationPointAdded
    Debug.WriteLine("AnnotatedImage.AnnotationCreationPointAdded")
    RaiseEvent AnnotationCreationPointAdded(objAnnotation)
  End Sub

  Private Sub objAnnotations_AnnotationCreationPointRemoved(ByVal objAnnotation As ClassesYVDH.Annotations.Annotation) Handles objAnnotations.AnnotationCreationPointRemoved
    Debug.WriteLine("AnnotatedImage.AnnotationCreationPointRemoved")
    RaiseEvent AnnotationCreationPointRemoved(objAnnotation)
  End Sub

#End Region
End Class

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here


Written By
Software Developer (Senior)
Belgium Belgium
Physicist, Biomedical Engineer, Phd in engineering. Specific expertise is in medical photography and it's related image processing, and in colourimetry.

Comments and Discussions