I want some Moore

Blog about stuff and things and stuff...
mostly about SQL server and .Net
posts - 155, comments - 1387, trackbacks - 33

My Links

SQLTeam.com Links

News

Hi! My name is 
Mladen Prajdić  I'm from Slovenia and I'm currently working as a .Net (C#) and SQL Server developer. I'm also a MCP and MCTS for SQL Server. 
Welcome to my blog.

Search this Blog
 

My Blog Feed via Email


Get your Google PageRank
Users Online: who's online

Article Categories

Archives

Post Categories

Cool software

Other Blogs

Other stuff

SQL stuff

Watershed Image Segmentation in C#

Lately I've been doing some image processing work using the most amazing AForge.NET open source library written in C#.

This library contains a whole bunch of useful filters you can play with but unfortunately it doesn't contain any filter for segmentation.

This is my implementation of Grayscale Immersion Watershed Segmentation in C# based on the

Vincent-Soille watershed algorithm which can be found in The Watershed Transform: Defnitions, Algorithms and Parallelization Strategies

paper by Jos B.T.M. Roerdink and Arnold Meijster on page 15. For watershed to work properly the areas you're trying to segment

have to be darker than the background. Blurring is also an important part of the image pre-processing since the algorithm

over segments non-blurred images. An example picture of watershed segmentation:

testPic testPic_Watershed
Original picture Segmented picture

 

using System;
using System.Collections.Generic;
using System.Text;

namespace Segmentation
{
    public class WatershedPixel
    {
        public int X;
        public int Y;
        public int Height;
        // labels the pixel as belonging to a unique basin or as a part of the watershed line
        public int Label;
        // Distance is a work image of distances
        public int Distance;

        public WatershedPixel()
        {
            this.X = -1;
            this.Y = -1;
            this.Height = -1000;
            this.Label = -1000;
            this.Distance = -1000;
        }

        public WatershedPixel(int x, int y)
        {
            this.X = x;
            this.Y = y;
            this.Height = -1000;
            this.Label = WatershedCommon.INIT;
            this.Distance = 0;
        }

        public WatershedPixel(int x, int y, int height)
        {
            this.X = x;
            this.Y = y;
            this.Height = height;
            this.Label = WatershedCommon.INIT;
            this.Distance = 0;
        }

        public override bool Equals(Object obj)
        {
            WatershedPixel p = (WatershedPixel)obj;
            return (X == p.X && X == p.Y);
        }

        public override int GetHashCode()
        {
            return X;
        }
        public override string ToString()
        {
            return "Height = " + Height + "; X = " + X.ToString() + ", Y = " + Y.ToString() + 
                   "; Label = " + Label.ToString() + "; Distance = " + Distance.ToString();
        }
    }
    
    public class WatershedGrayscale : FilterGrayToGray
    {
        #region Variables
        private WatershedPixel FictitiousPixel = new WatershedPixel();
        private int _currentLabel = 0;
        private int _currentDistance = 0;
        private FifoQueue _fifoQueue = new FifoQueue();
        // each pixel can be accesesd from 2 places: a dictionary for faster direct lookup of neighbouring pixels 
        // or from a height ordered list
        // sorted array of pixels according to height
        private List<List<WatershedPixel>> _heightSortedList;
        // string in the form "X,Y" is used as a key for the dictionary lookup of a pixel
        private Dictionary<string, WatershedPixel> _pixelMap;
        private int _watershedPixelCount = 0;
        private int _numberOfNeighbours = 8;
        private bool _borderInWhite;
        private int _pictureWidth = 0;
        private int _pictureHeight = 0;
        #endregion

        #region Constructors
        public WatershedGrayscale()
            : this(8)
        {}

        public WatershedGrayscale(int numberOfNeighbours)
        {
            if (numberOfNeighbours != 8 && numberOfNeighbours != 4)
                throw new Exception("Invalid number of neighbour pixels to check. Valid values are 4 and 8.");
            _borderInWhite = true;
            _numberOfNeighbours = numberOfNeighbours;
            _heightSortedList = new List<List<WatershedPixel>>(256);
            for (int i = 0; i < 256; i++)
                _heightSortedList.Add(new List<WatershedPixel>());
        }
        #endregion

        #region Properties
        /// <summary>
        /// number of neighbours to check for each pixel. valid values are 8 and 4
        /// </summary>
        public int NumberOfNeighbours
        {
            get { return _numberOfNeighbours; }
            set
            {
                if (value != 8 && value != 4)
                    throw new Exception("Invalid number of neighbour pixels to check. Valid values are 4 and 8.");
                _numberOfNeighbours = value;
            }
        }

        /// <summary>
        /// Number of labels/basins found
        /// </summary>
        public int LabelCount
        {
            get { return _currentLabel; }
            set { _currentLabel = value; }
        }

        /// <summary>
        /// True: border is drawn in white. False: border is drawn in black
        /// </summary>
        /// <value></value>
        public bool BorderInWhite
        {
            get { return _borderInWhite; }
            set { _borderInWhite = value; }
        }
        #endregion

        private void CreatePixelMapAndHeightSortedArray(BitmapData data)
        {
            _pictureWidth = data.Width;
            _pictureHeight = data.Height;
            // pixel map holds every pixel thus size of (_pictureWidth * _pictureHeight)
            _pixelMap = new Dictionary<string, WatershedPixel>(_pictureWidth * _pictureHeight);
            unsafe
            {                
                int offset = data.Stride - data.Width;
                byte* ptr = (byte*)(data.Scan0);

                // get histogram of all values in grey = height
                for (int y = 0; y < data.Height; y++)
                {
                    for (int x = 0; x < data.Width; x++, ptr++)
                    {
                        WatershedPixel p = new WatershedPixel(x, y, *ptr);
                        // add every pixel to the pixel map
                        _pixelMap.Add(p.X.ToString() + "," + p.Y.ToString(), p);
                        _heightSortedList[*ptr].Add(p);
                    }
                    ptr += offset;
                }
            }
            this._currentLabel = 0;
        }

        private void Segment()
        {            
            // Geodesic SKIZ (skeleton by influence zones) of each level height
            for (int h = 0; h < _heightSortedList.Count; h++)
            {
                // get all pixels for current height
                foreach (WatershedPixel heightSortedPixel in _heightSortedList[h])
                {
                    heightSortedPixel.Label = WatershedCommon.MASK;                    
                    // for each pixel on current height get neighbouring pixels
                    List<WatershedPixel> neighbouringPixels = GetNeighbouringPixels(heightSortedPixel);
                    // initialize queue with neighbours at level h of current basins or watersheds
                    foreach (WatershedPixel neighbouringPixel in neighbouringPixels)
                    {
                        if (neighbouringPixel.Label > 0 || neighbouringPixel.Label == WatershedCommon.WSHED)
                        {
                            heightSortedPixel.Distance = 1;
                            _fifoQueue.AddToEnd(heightSortedPixel);
                            break;
                        }                        
                    }
                }
                _currentDistance = 1;
                _fifoQueue.AddToEnd(FictitiousPixel);
                // extend basins
                while (true)
                {
                    WatershedPixel p = _fifoQueue.RemoveAtFront();
                    if (p.Equals(FictitiousPixel))
                    {
                        if (_fifoQueue.IsEmpty)
                            break;
                        else
                        {
                            _fifoQueue.AddToEnd(FictitiousPixel);
                            _currentDistance++;
                            p = _fifoQueue.RemoveAtFront();
                        }
                    }
                    List<WatershedPixel> neighbouringPixels = GetNeighbouringPixels(p);
                    // labelling p by inspecting neighbours
                    foreach (WatershedPixel neighbouringPixel in neighbouringPixels)
                    {
                        // neighbouringPixel belongs to an existing basin or to watersheds
                        // in the original algorithm the condition is:
                        //   if (neighbouringPixel.Distance < currentDistance && 
                        //      (neighbouringPixel.Label > 0 || neighbouringPixel.Label == WatershedCommon.WSHED))
                        //   but this returns incomplete borders so the this one is used                        
                        if (neighbouringPixel.Distance <= _currentDistance && 
                           (neighbouringPixel.Label > 0 || neighbouringPixel.Label == WatershedCommon.WSHED))
                        {
                            if (neighbouringPixel.Label > 0)
                            {
                                // the commented condition is also in the original algorithm 
                                // but it also gives incomplete borders
                                if (p.Label == WatershedCommon.MASK /*|| p.Label == WatershedCommon.WSHED*/) 
                                    p.Label = neighbouringPixel.Label;
                                else if (p.Label != neighbouringPixel.Label)
                                {
                                    p.Label = WatershedCommon.WSHED;
                                    _watershedPixelCount++;
                                }
                            }
                            else if (p.Label == WatershedCommon.MASK)
                            {
                                p.Label = WatershedCommon.WSHED;
                                _watershedPixelCount++;
                            }
                        }
                        // neighbouringPixel is plateau pixel
                        else if (neighbouringPixel.Label == WatershedCommon.MASK && neighbouringPixel.Distance == 0)
                        {
                            neighbouringPixel.Distance = _currentDistance + 1;
                            _fifoQueue.AddToEnd(neighbouringPixel);
                        }
                    }
                }
                // detect and process new minima at height level h
                foreach (WatershedPixel p in _heightSortedList[h])
                {
                    // reset distance to zero
                    p.Distance = 0;
                    // if true then p is inside a new minimum 
                    if (p.Label == WatershedCommon.MASK)
                    {
                        // create new label
                        _currentLabel++;
                        p.Label = _currentLabel;
                        _fifoQueue.AddToEnd(p);
                        while (!_fifoQueue.IsEmpty)
                        {
                            WatershedPixel q = _fifoQueue.RemoveAtFront();
                            // check neighbours of q
                            List<WatershedPixel> neighbouringPixels = GetNeighbouringPixels(q);
                            foreach (WatershedPixel neighbouringPixel in neighbouringPixels)
                            {
                                if (neighbouringPixel.Label == WatershedCommon.MASK)
                                {
                                    neighbouringPixel.Label = _currentLabel;
                                    _fifoQueue.AddToEnd(neighbouringPixel);
                                }
                            }
                        }
                    }
                }
            }
        }

        private List<WatershedPixel> GetNeighbouringPixels(WatershedPixel centerPixel)
        {
            List<WatershedPixel> temp = new List<WatershedPixel>();
            if (_numberOfNeighbours == 8)
            {
                /*
                CP = Center pixel
                (X,Y) -- get all 8 connected 
                |-1,-1|0,-1|1,-1|
                |-1, 0| CP |1, 0|
                |-1,+1|0,+1|1,+1|
                */                
                // -1, -1                
                if ((centerPixel.X - 1) >= 0 && (centerPixel.Y - 1) >= 0)
                    temp.Add(_pixelMap[(centerPixel.X - 1).ToString() + "," + (centerPixel.Y - 1).ToString()]);
                //  0, -1
                if ((centerPixel.Y - 1) >= 0)
                    temp.Add(_pixelMap[centerPixel.X.ToString() + "," + (centerPixel.Y - 1).ToString()]);
                //  1, -1
                if ((centerPixel.X + 1) < _pictureWidth && (centerPixel.Y - 1) >= 0)
                    temp.Add(_pixelMap[(centerPixel.X + 1).ToString() + "," + (centerPixel.Y - 1).ToString()]);
                // -1, 0
                if ((centerPixel.X - 1) >= 0)
                    temp.Add(_pixelMap[(centerPixel.X - 1).ToString() + "," + centerPixel.Y.ToString()]);
                //  1, 0
                if ((centerPixel.X + 1) < _pictureWidth)
                    temp.Add(_pixelMap[(centerPixel.X + 1).ToString() + "," + centerPixel.Y.ToString()]);
                // -1, 1
                if ((centerPixel.X - 1) >= 0 && (centerPixel.Y + 1) < _pictureHeight)
                    temp.Add(_pixelMap[(centerPixel.X - 1).ToString() + "," + (centerPixel.Y + 1).ToString()]);
                //  0, 1
                if ((centerPixel.Y + 1) < _pictureHeight)
                    temp.Add(_pixelMap[centerPixel.X.ToString() + "," + (centerPixel.Y + 1).ToString()]);
                //  1, 1
                if ((centerPixel.X + 1) < _pictureWidth && (centerPixel.Y + 1) < _pictureHeight)
                    temp.Add(_pixelMap[(centerPixel.X + 1).ToString() + "," + (centerPixel.Y + 1).ToString()]);
            }
            else
            {
                /*
                CP = Center pixel, N/A = not used
                (X,Y) -- get only 4 connected 
                | N/A |0,-1| N/A |
                |-1, 0| CP |+1, 0|
                | N/A |0,+1| N/A |
                */
                //  -1, 0
                if ((centerPixel.X - 1) >= 0)
                    temp.Add(_pixelMap[(centerPixel.X - 1).ToString() + "," + centerPixel.Y.ToString()]);
                //  0, -1
                if ((centerPixel.Y - 1) >= 0)
                    temp.Add(_pixelMap[centerPixel.X.ToString() + "," + (centerPixel.Y - 1).ToString()]);
                //  1, 0
                if ((centerPixel.X + 1) < _pictureWidth)
                    temp.Add(_pixelMap[(centerPixel.X + 1).ToString() + "," + centerPixel.Y.ToString()]);
                //  0, 1
                if ((centerPixel.Y + 1) < _pictureHeight)
                    temp.Add(_pixelMap[centerPixel.X.ToString() + "," + (centerPixel.Y + 1).ToString()]);
            }
            return temp;
        }

        private void DrawWatershedLines(BitmapData data)
        {
            if (_watershedPixelCount == 0)
                return;

            byte watershedColor = 255;
            if (!_borderInWhite)
                watershedColor = 0;
            unsafe
            {
                int offset = data.Stride - data.Width;
                byte* ptr = (byte*)(data.Scan0);

                for (int y = 0; y < data.Height; y++)
                {
                    for (int x = 0; x < data.Width; x++, ptr++)
                    {
                        // if the pixel in our map is watershed pixel then draw it
                        if (_pixelMap[x.ToString() + "," + y.ToString()].Label == WatershedCommon.WSHED)
                            *ptr = watershedColor;
                    }
                    ptr += offset;
                }
            }
        }

        protected override void ProcessFilter(BitmapData imageData)
        {
            CreatePixelMapAndHeightSortedArray(imageData);
            Segment();
            DrawWatershedLines(imageData);
        }
    }
    
    public class FifoQueue
    {
        List<WatershedPixel> queue = new List<WatershedPixel>();

        public int Count
        {
            get { return queue.Count; }
        }

        public void AddToEnd(WatershedPixel p)
        {
            queue.Add(p);
        }

        public WatershedPixel RemoveAtFront()
        {
            WatershedPixel temp = queue[0];
            queue.RemoveAt(0);
            return temp;
        }

        public bool IsEmpty
        {
            get { return (queue.Count == 0); }
        }

        public override string ToString()
        {
            return base.ToString() + " Count = " + queue.Count.ToString();
        }
    }
    
    public class WatershedCommon
    {
        #region Constants
        public const int INIT = -1;
        public const int MASK = -2;
        public const int WSHED = 0;
        #endregion
    }
}

 

This filter will also become the part of the AForge.NET library in the future so to use this algorithm you have to have it installed.

The performance can be improved as I've gone for the readability factor since performance wasn't very important on my project.

 

kick it on DotNetKicks.com
 

Print | posted on Monday, February 11, 2008 6:46 PM

Feedback

No comments posted yet.

Post Comment

Title  
Name  
Email
Url
Comment   
Please add 1 and 7 and type the answer here:

Powered by: