0

I've done some research but drawn a blank. Is there anyway to achieve image outlining with GDI? I.e. transform this:

enter image description here

into this:

enter image description here

With the ability to have different stroke thicknesses and colors. In either vb.net or c# for winforms?

stigzler
  • 793
  • 2
  • 12
  • 29

1 Answers1

0

OK. Cracked it. This code is based on this answer HERE . I'm posting the answer here for anyone who's looking to do the same. Full details are HERE. You'll need to compile and use the library Gaussian Blur (aka SuperFastBlur) available HERE. Then you include the code at the end of this post and use it via:

Dim stroke As New Stroke
_image = stroke.Apply(_image, _strokeWidth, _strokeColor, _strokeBlur, _strokeLineJoin)

The parameters should be self explanatory. Some results:

enter image description here enter image description here enter image description here enter image description here

Finally, the code:

Imports System.Drawing
Imports System.Drawing.Drawing2D
Imports System.Drawing.Imaging
Imports System.Runtime.InteropServices
Imports SuperfastBlur

Public Class Stroke
    Private stride As Integer = 0
    Private visited As Integer() = Nothing
    Private bytes As Byte() = Nothing
    Private borderdata As PointData = Nothing
    Private size As Size = Size.Empty
    Private outside As Boolean = False
    Private zeropoint As Point = New Point(-1, -1)


    Public Function Apply(bmp As Bitmap, strokeWidth As Integer, strokeColor As Color, strokeBlur As Integer, LineJoin As LineJoin) As Bitmap

        Dim borderedBmp As Bitmap = NewBitmapWithBorder(bmp, strokeWidth)

        Dim imgPointsArray As List(Of Point()) = Find(borderedBmp)

        Dim newBmp As Bitmap = New Bitmap(borderedBmp.Width, borderedBmp.Height)

        Using g As Graphics = Graphics.FromImage(newBmp)

            With g
                .PixelOffsetMode = PixelOffsetMode.HighQuality
                .CompositingMode = CompositingMode.SourceOver
                .InterpolationMode = InterpolationMode.HighQualityBilinear
                .SmoothingMode = SmoothingMode.AntiAlias
                .CompositingQuality = CompositingQuality.HighQuality
            End With

            For Each pointsArray As Point() In imgPointsArray

                DrawOutline(g, pointsArray, strokeColor, strokeWidth, LineCap.Round, LineJoin)

            Next

        End Using

        If strokeBlur > 0 Then
            Dim gblur = New GaussianBlur(newBmp)
            newBmp = gblur.Process(strokeBlur)
        End If

        Using g As Graphics = Graphics.FromImage(newBmp)
            g.DrawImage(borderedBmp, 0, 0)
        End Using

        Return newBmp

    End Function


    Private Sub DrawOutline(g As Graphics, points As Point(), penColor As Color, penSize As Single, cap As LineCap, lJoin As LineJoin)

        Using gp As GraphicsPath = New GraphicsPath(FillMode.Winding),
                                    pen As New Pen(penColor, penSize) With {.LineJoin = lJoin, .StartCap = cap, .EndCap = cap},
                                    widenPen As New Pen(penColor, 1.0F)
            gp.AddCurve(points)
            gp.Widen(widenPen)
            g.DrawPath(pen, gp)
        End Using

    End Sub


    Private Function NewBitmapWithBorder(ByVal bmp As Bitmap, ByVal Optional borderSize As Integer = 0) As Bitmap
        Dim newWidth As Integer = bmp.Width + (borderSize * 2)
        Dim newHeight As Integer = bmp.Height + (borderSize * 2)
        Dim newImage As Image = New Bitmap(newWidth, newHeight)

        Using gfx As Graphics = Graphics.FromImage(newImage)

            Using border As Brush = New SolidBrush(Color.Transparent)
                ' gfx.FillRectangle(border, 0, 0, newWidth, newHeight)
            End Using

            gfx.DrawImage(bmp, New Rectangle(borderSize, borderSize, bmp.Width, bmp.Height))
        End Using

        Return CType(newImage, Bitmap)
    End Function

    Public Function Find(ByVal bmp As Bitmap, ByVal Optional outside As Boolean = True) As List(Of Point())
        Me.outside = outside
        Dim border As List(Of Point) = New List(Of Point)()
        Dim bmpdata As BitmapData = bmp.LockBits(New Rectangle(0, 0, bmp.Width, bmp.Height), ImageLockMode.[ReadOnly], PixelFormat.Format32bppArgb)
        stride = bmpdata.Stride
        bytes = New Byte(bmp.Width * bmp.Height * 4 - 1) {}
        size = bmp.Size
        Marshal.Copy(bmpdata.Scan0, bytes, 0, bytes.Length)
        borderdata = getBorderData(bytes)
        bmp.UnlockBits(bmpdata)
        Dim regions As List(Of List(Of Point)) = New List(Of List(Of Point))()

        While borderdata.PointCount > 0
            Dim region As List(Of Point) = New List(Of Point)()
            Dim valid As Boolean = True
            Dim startpos As Point = getFirstPoint(borderdata)
            visited = New Integer(bmp.Size.Width * bmp.Size.Height - 1) {}
            region.Add(startpos)
            Dim current As Point = getNextPoint(startpos)

            If current <> zeropoint Then
                visited(current.Y * bmp.Width + current.X) += 1
                region.Add(current)
            End If

            If current = zeropoint Then valid = False

            While Not current.Equals(startpos) AndAlso valid
                Dim pos = current

                If visited(current.Y * bmp.Width + current.X) < 2 Then
                    current = getNextPoint(pos)
                    visited(pos.Y * bmp.Width + pos.X) += 1
                    If current = zeropoint Then current = getNextPointBackwards(pos)
                Else
                    current = getNextPointBackwards(pos)
                End If

                If current = zeropoint Then
                    valid = False
                    Exit While
                End If

                visited(current.Y * bmp.Width + current.X) += 1
                region.Add(current)
            End While

            For Each p In region
                borderdata.SetPoint(p.Y * bmp.Width + p.X, False)
            Next

            If valid Then regions.Add(region)
        End While

        For Each region In regions
            Dim duplicatedpos As Integer = -1
            Dim duplicatecheck As Boolean() = New Boolean(size.Width * size.Height - 1) {}
            Dim length As Integer = region.Count

            For i As Integer = 0 To length - 1
                Dim p = region(i)

                If duplicatecheck(p.Y * size.Width + p.X) Then
                    duplicatedpos = i - 1
                    Exit For
                End If

                duplicatecheck(p.Y * size.Width + p.X) = True
            Next

            If duplicatedpos = -1 Then Continue For
            If duplicatedpos <> ((region.Count - 1) / 2) Then Continue For
            Dim reversed As Boolean = True

            For i As Integer = 0 To duplicatedpos - 1

                If region(duplicatedpos - i - 1) <> region(duplicatedpos + i + 1) Then
                    reversed = False
                    Exit For
                End If
            Next

            If Not reversed Then Continue For
            region.RemoveRange(duplicatedpos + 1, region.Count - duplicatedpos - 1)
        Next

        Dim tempregions As List(Of List(Of Point)) = New List(Of List(Of Point))(regions)
        regions.Clear()
        Dim connected As Boolean = True

        While connected
            connected = False

            For Each region In tempregions
                Dim connectionpos As Integer = -1
                Dim connectionregion As Integer = -1
                Dim pointstart As Point = region.First()
                Dim pointend As Point = region.Last()

                For ir As Integer = 0 To regions.Count - 1
                    Dim otherregion = regions(ir)
                    If region Is otherregion Then Continue For

                    For ip As Integer = 0 To otherregion.Count - 1
                        Dim p = otherregion(ip)

                        If (isConnected(pointstart, p) AndAlso isConnected(pointend, p)) OrElse (isConnected(pointstart, p) AndAlso isConnected(pointstart, p)) Then
                            connectionregion = ir
                            connectionpos = ip
                        End If

                        If (isConnected(pointend, p) AndAlso isConnected(pointend, p)) Then
                            region.Reverse()
                            connectionregion = ir
                            connectionpos = ip
                        End If
                    Next
                Next

                If connectionpos = -1 Then
                    regions.Add(region)
                Else
                    regions(connectionregion).InsertRange(connectionpos, region)
                End If
            Next

            tempregions = New List(Of List(Of Point))(regions)
            regions.Clear()
        End While

        Dim returnregions As List(Of Point()) = New List(Of Point())()

        For Each region In tempregions
            returnregions.Add(region.ToArray())
        Next

        Return returnregions
    End Function

    Private Function isConnected(ByVal p0 As Point, ByVal p1 As Point) As Boolean
        If p0.X = p1.X AndAlso p0.Y - 1 = p1.Y Then Return True
        If p0.X + 1 = p1.X AndAlso p0.Y - 1 = p1.Y Then Return True
        If p0.X + 1 = p1.X AndAlso p0.Y = p1.Y Then Return True
        If p0.X + 1 = p1.X AndAlso p0.Y + 1 = p1.Y Then Return True
        If p0.X = p1.X AndAlso p0.Y + 1 = p1.Y Then Return True
        If p0.X - 1 = p1.X AndAlso p0.Y + 1 = p1.Y Then Return True
        If p0.X - 1 = p1.X AndAlso p0.Y = p1.Y Then Return True
        If p0.X - 1 = p1.X AndAlso p0.Y - 1 = p1.Y Then Return True
        Return False
    End Function

    Private Function getNextPoint(ByVal pos As Point) As Point
        If pos.Y > 0 Then
            Dim x As Integer = pos.X
            Dim y As Integer = pos.Y - 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.Y > 0 AndAlso pos.X < size.Width - 1 Then
            Dim x As Integer = pos.X + 1
            Dim y As Integer = pos.Y - 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.X < size.Width - 1 Then
            Dim x As Integer = pos.X + 1
            Dim y As Integer = pos.Y

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.X < size.Width - 1 AndAlso pos.Y < size.Height - 1 Then
            Dim x As Integer = pos.X + 1
            Dim y As Integer = pos.Y + 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.Y < size.Height - 1 Then
            Dim x As Integer = pos.X
            Dim y As Integer = pos.Y + 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.Y < size.Height - 1 AndAlso pos.X > 0 Then
            Dim x As Integer = pos.X - 1
            Dim y As Integer = pos.Y + 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.X > 0 Then
            Dim x As Integer = pos.X - 1
            Dim y As Integer = pos.Y

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        If pos.X > 0 AndAlso pos.Y > 0 Then
            Dim x As Integer = pos.X - 1
            Dim y As Integer = pos.Y - 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If
            End If
        End If

        Return zeropoint
    End Function

    Private Function getNextPointBackwards(ByVal pos As Point) As Point
        Dim backpoint As Point = zeropoint
        Dim trys As Integer = 0

        If pos.X > 0 AndAlso pos.Y > 0 Then
            Dim x As Integer = pos.X - 1
            Dim y As Integer = pos.Y - 1

            If ValidPoint(x, y) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.X > 0 Then
            Dim x As Integer = pos.X - 1
            Dim y As Integer = pos.Y

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.Y < size.Height - 1 AndAlso pos.X > 0 Then
            Dim x As Integer = pos.X - 1
            Dim y As Integer = pos.Y + 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.Y < size.Height - 1 Then
            Dim x As Integer = pos.X
            Dim y As Integer = pos.Y + 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.X < size.Width - 1 AndAlso pos.Y < size.Height - 1 Then
            Dim x As Integer = pos.X + 1
            Dim y As Integer = pos.Y + 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.X < size.Width - 1 Then
            Dim x As Integer = pos.X + 1
            Dim y As Integer = pos.Y

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.Y > 0 AndAlso pos.X < size.Width - 1 Then
            Dim x As Integer = pos.X + 1
            Dim y As Integer = pos.Y - 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        If pos.Y > 0 Then
            Dim x As Integer = pos.X
            Dim y As Integer = pos.Y - 1

            If (ValidPoint(x, y)) AndAlso HasNeighbor(x, y) Then

                If visited(y * size.Width + x) = 0 Then
                    Return New Point(x, y)
                End If

                If backpoint = zeropoint OrElse trys > visited(y * size.Width + x) Then
                    backpoint = New Point(x, y)
                    trys = visited(y * size.Width + x)
                End If
            End If
        End If

        Return backpoint
    End Function

    Private Function ValidPoint(ByVal x As Integer, ByVal y As Integer) As Boolean
        Return (borderdata(y * size.Width + x))
    End Function

    Private Function HasNeighbor(ByVal x As Integer, ByVal y As Integer) As Boolean
        If y > 0 Then

            If Not borderdata((y - 1) * size.Width + x) Then
                Return True
            End If
        ElseIf ValidPoint(x, y) Then
            Return True
        End If

        If x < size.Width - 1 Then

            If Not borderdata(y * size.Width + (x + 1)) Then
                Return True
            End If
        ElseIf ValidPoint(x, y) Then
            Return True
        End If

        If y < size.Height - 1 Then

            If Not borderdata((y + 1) * size.Width + x) Then
                Return True
            End If
        ElseIf ValidPoint(x, y) Then
            Return True
        End If

        If x > 0 Then

            If Not borderdata(y * size.Width + (x - 1)) Then
                Return True
            End If
        ElseIf ValidPoint(x, y) Then
            Return True
        End If

        Return False
    End Function

    Private Function getFirstPoint(ByVal data As PointData) As Point
        Dim startpos As Point = zeropoint

        For y As Integer = 0 To size.Height - 1

            For x As Integer = 0 To size.Width - 1

                If data(y * size.Width + x) Then
                    startpos = New Point(x, y)
                    Return startpos
                End If
            Next
        Next

        Return startpos
    End Function

    Private Function getBorderData(ByVal bytes As Byte()) As PointData
        Dim isborderpoint As PointData = New PointData(size.Height * size.Width)
        Dim prevtrans As Boolean = False
        Dim currenttrans As Boolean = False

        For y As Integer = 0 To size.Height - 1
            prevtrans = False

            For x As Integer = 0 To size.Width

                If x = size.Width Then

                    If Not prevtrans Then
                        isborderpoint.SetPoint(y * size.Width + x - 1, True)
                    End If

                    Continue For
                End If

                currenttrans = bytes(y * stride + x * 4 + 3) = 0
                If x = 0 AndAlso Not currenttrans Then isborderpoint.SetPoint(y * size.Width + x, True)
                If prevtrans AndAlso Not currenttrans Then isborderpoint.SetPoint(y * size.Width + x - 1, True)
                If Not prevtrans AndAlso currenttrans AndAlso x <> 0 Then isborderpoint.SetPoint(y * size.Width + x, True)
                prevtrans = currenttrans
            Next
        Next

        For x As Integer = 0 To size.Width - 1
            prevtrans = False

            For y As Integer = 0 To size.Height

                If y = size.Height Then

                    If Not prevtrans Then
                        isborderpoint.SetPoint((y - 1) * size.Width + x, True)
                    End If

                    Continue For
                End If

                currenttrans = bytes(y * stride + x * 4 + 3) = 0
                If y = 0 AndAlso Not currenttrans Then isborderpoint.SetPoint(y * size.Width + x, True)
                If prevtrans AndAlso Not currenttrans Then isborderpoint.SetPoint((y - 1) * size.Width + x, True)
                If Not prevtrans AndAlso currenttrans AndAlso y <> 0 Then isborderpoint.SetPoint(y * size.Width + x, True)
                prevtrans = currenttrans
            Next
        Next

        Return isborderpoint
    End Function
End Class

Class PointData
    Private points As Boolean() = Nothing
    Private validpoints As Integer = 0

    Public Sub New(ByVal length As Integer)
        points = New Boolean(length - 1) {}
    End Sub

    Public ReadOnly Property PointCount As Integer
        Get
            Return validpoints
        End Get
    End Property

    Public Sub SetPoint(ByVal pos As Integer, ByVal state As Boolean)
        If points(pos) <> state Then

            If state Then
                validpoints += 1
            Else
                validpoints -= 1
            End If
        End If

        points(pos) = state
    End Sub

    Default Public ReadOnly Property Item(ByVal pos As Integer) As Boolean
        Get
            Return points(pos)
        End Get
    End Property
End Class


stigzler
  • 793
  • 2
  • 12
  • 29