1. 程式人生 > >C# 連通域及其質心標記

C# 連通域及其質心標記

連通域在影象處理領域用的還是比較多的,在網上搜索連通域提取的演算法和程式碼,比較多的是用C++寫的,瀏覽次數比較多的是這篇部落格,但由於一直是用C#編寫程式碼,所以仿照那篇部落格中的C++程式,將其改為了C#程式,同時在此基礎上添加了質心計算和標記函式。

構造了兩個結構體,EqualRun用於記錄兩個等價run,centroid用於儲存與質心計算相關的引數。

 public struct EqualRun
    {
        public int firstRun;
        public int secondRun;
    };

    public struct Centroid
    {
        public int sumx;
        public int sumy;
        public int area;
    };

此三個函式:fillRunVectors、firstpass、replaceSameLabel是仿照部落格寫的。函式centroidCalculate用於計算連通域的質心。函式drawCaliResult用於在圖中標記出質心位置。

 //fillRunVectors函式完成所有團的查詢與記錄
        public void fillRunVectors(Bitmap bwBitmap,ref int NumberOfRuns, ref List<int> stRun,
            ref List<int> enRun, ref List<int> rowRun, out List<Point> stPoint, out List<Point> endPoint)
        {
            stPoint = new List<Point>();
            endPoint = new List<Point>();
            BitmapData bwBmpData = bwBitmap.LockBits(new Rectangle(0, 0, bwBitmap.Width, bwBitmap.Height), ImageLockMode.ReadWrite, bwBitmap.PixelFormat);
            byte* sorPtr = (byte*)bwBmpData.Scan0;

            for (int i = 0; i<bwBitmap.Height; i++)//行
	        {
		        if (sorPtr[i* bwBmpData.Stride] == 255)//每行的第一個元素
		        {
			        NumberOfRuns++;
                    stRun.Add(0);
                    rowRun.Add(i);
                    stPoint.Add(new Point(0, i));
		        }
		        for (int j = 1; j<bwBitmap.Width; j++)//列
		        {
			        if (sorPtr[i * bwBmpData.Stride + j - 1] == 0 && sorPtr[i * bwBmpData.Stride + j] == 255)//新增一個團
			        {
				        NumberOfRuns++;
                        stRun.Add(j);
                        rowRun.Add(i);
                        stPoint.Add(new Point(j, i));
                    }
			        else if (sorPtr[i * bwBmpData.Stride + j - 1] == 255 && sorPtr[i * bwBmpData.Stride + j] == 0)//結束一個團
			        {
                        enRun.Add(j - 1);
                        endPoint.Add(new Point(j - 1, i));
			        }
		        }
		        if (sorPtr[i * bwBmpData.Stride + bwBmpData.Width - 1]==255)//每行的最後一個元素
		        {
			        enRun.Add(bwBitmap.Width - 1);
                    endPoint.Add(new Point(bwBitmap.Width - 1, i));
                }
            }
            bwBitmap.UnlockBits(bwBmpData);
        }

        //firstPass函式完成團的標記與等價對列表的生成 offset=1,8鄰域,offset=0,四鄰域
        public void firstPass(ref List<int> stRun, ref List<int> enRun, ref List<int> rowRun, ref int NumberOfRuns,
            ref List<int> runLabels, ref List<EqualRun> equivalences, int offset)
        {
            //runLabels = new List<int>(NumberOfRuns);//可能會出問題
            for (int i = 0; i < NumberOfRuns; i++)
            {
                runLabels.Add(0);
            }
            int idxLabel = 1;
            int curRowIdx = 0;
            int firstRunOnCur = 0;
            int firstRunOnPre = 0;
            int lastRunOnPre = -1;
            for (int i = 0; i < NumberOfRuns; i++)
            {
                // 如果是該行的第一個run,則更新上一行第一個run第最後一個run的序號
                if (rowRun[i] != curRowIdx)
                {
                    curRowIdx = rowRun[i]; // 更新行的序號
                    firstRunOnPre = firstRunOnCur;
                    lastRunOnPre = i - 1;
                    firstRunOnCur = i;
                }
                // 遍歷上一行的所有run,判斷是否與當前run有重合的區域
                for (int j = firstRunOnPre; j <= lastRunOnPre; j++)
                {
                    // 區域重合 且 處於相鄰的兩行
                    if (stRun[i] <= enRun[j] + offset && enRun[i] >= stRun[j] - offset && rowRun[i] == rowRun[j] + 1)
                    {
                        if (runLabels[i] == 0) // 沒有被標號過
                            runLabels[i] = runLabels[j];
                        else if (runLabels[i] != runLabels[j])// 已經被標號            
                        {
                            EqualRun equalRun;
                            equalRun.firstRun = runLabels[i];
                            equalRun.secondRun = runLabels[j];
                            equivalences.Add(equalRun); // 儲存等價對
                        }
                    }
                }
                if (runLabels[i] == 0) // 沒有與前一列的任何run重合
                {
                    runLabels[i] = idxLabel++;
                }
            }
        }

        //每個等價表用一個vector<int>來儲存,等價對列表儲存在map<pair<int,int>>裡
        public void replaceSameLabel(ref List<int> runLabels, ref List<EqualRun> equivalence, out List<List<int>> equaList)
        {
            int maxLabel = runLabels.Max();//找到最大的標號
            bool[,] eqTab = new bool[maxLabel + 1, maxLabel + 1];
            for(int i = 0; i < maxLabel; i++)
            {
                for(int j = 0; j < maxLabel; j++)
                {
                    eqTab[i, j] = false;
                }
            }
            foreach(var item in equivalence)
            {
                eqTab[item.firstRun, item.secondRun] = true;
                eqTab[item.secondRun, item.firstRun] = true;
            }
            List<int> labelFlag = new List<int>();
            for (int i = 0; i < maxLabel + 1; i++)
            {
                labelFlag.Add(0);
            }

            equaList = new List<List<int>>();//等價連結串列
            /*
             遍歷所有的團標號,來看每個標號應該加入哪個連通域,並賦連通域標號(labelFlag)。
             若當前的標號不屬於任何一個連通域,則新給它一個連通域標號(labelFlag),並把它加入當前的連通域(tempList.push_back()),
             遍歷當前連通域內的所有標號,將包含其中標號的所有等價對都加入到當前連通域內。
            */
            int labelFlagNum = 1;
            for (int i = 1; i <= maxLabel; i++)
            {
                if (labelFlag[i] != 0)
                {
                    continue;
                }
                labelFlag[i] = labelFlagNum;
                List<int> tempList = new List<int>();
                tempList.Add(i);
                for (int j = 0; j < tempList.Count; j++)
                {
                    for (int k = 0; k != maxLabel; k++)
                    {
                        if (eqTab[tempList[j], k] && labelFlag[k]==0) //說明標號k+1與j連通,並且k+1沒有被表示過,不屬於任何連通域
                        {
                            tempList.Add(k);
                            labelFlag[k] = labelFlagNum;
                        }
                    }
                }
                equaList.Add(tempList);
                labelFlagNum++;
            }
            for (int i = 0; i != runLabels.Count; i++)
            {
                runLabels[i] = labelFlag[runLabels[i]];
            }
        }
public void centroidCalculate(ref List<Point> stPoint, ref List<Point> endPoint, ref List<int> runLabels, out List<Point> centroidPointList)
        {
            int maxLabel = runLabels.Max();//找到最大的標號
            List<Centroid> centroidList = new List<Centroid>();//儲存與質心計算有關的引數,總個數為連通域個數
            for(int i = 0; i < maxLabel; i++)
            {
                Centroid tempCentroid;
                tempCentroid.sumx = 0;
                tempCentroid.sumy = 0;
                tempCentroid.area = 0;
                centroidList.Add(tempCentroid);
            }
            for(int i = 0; i< runLabels.Count; i++)
            {
                int index = runLabels[i];
                int tempSumx = (stPoint[i].X + endPoint[i].X) * (endPoint[i].X - stPoint[i].X + 1) / 2;
                int tempSumy = (endPoint[i].X - stPoint[i].X + 1) * stPoint[i].Y;
                int tempArea = endPoint[i].X - stPoint[i].X + 1;
                int newSumx = centroidList[index - 1].sumx + tempSumx;
                int newSumy = centroidList[index - 1].sumy + tempSumy;
                int newArea = centroidList[index - 1].area + tempArea;
                Centroid newCentroid;
                newCentroid.sumx = newSumx; newCentroid.sumy = newSumy; newCentroid.area = newArea;
                centroidList[index - 1] = newCentroid;
            }
            centroidPointList = new List<Point>();
            for(int i = 0; i < maxLabel; i++)
            {
                Point tempPoint = new Point(centroidList[i].sumx / centroidList[i].area, centroidList[i].sumy / centroidList[i].area);
                centroidPointList.Add(tempPoint);
            }
        }
        /// <summary>
        /// 畫出連通域的質心
        /// </summary>
        /// <param name="oriBitmap">二值影象</param>
        /// <param name="centroidPointList">質心點集</param>
        /// <returns></returns>
        public Bitmap drawCaliResult(Bitmap oriBitmap, List<Point> centroidPointList)
        {
            Bitmap result = (Bitmap)oriBitmap.Clone();
            Graphics g;
            Color[] color = new Color[9];
            color[0] = Color.FromArgb(255, 0, 0);
            color[1] = Color.FromArgb(0, 255, 0);
            color[2] = Color.FromArgb(0, 0, 255);
            color[3] = Color.FromArgb(255, 255, 0);
            color[4] = Color.FromArgb(0, 255, 255);
            color[5] = Color.FromArgb(255, 0, 255);
            color[6] = Color.FromArgb(125, 0, 125);
            color[7] = Color.FromArgb(125, 125, 0);
            color[8] = Color.FromArgb(0, 125, 125);


            using (Image img = result)
            {
                //如果原圖片是索引畫素格式之列的,則需要轉換格式
                if (IsPixelFormatIndexed(img.PixelFormat))
                {
                    result = new Bitmap(img.Width, img.Height, PixelFormat.Format32bppArgb);
                    using (g = Graphics.FromImage(result))
                    {
                        g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.HighQualityBicubic;
                        g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.HighQuality;
                        g.CompositingQuality = System.Drawing.Drawing2D.CompositingQuality.HighQuality;
                        g.DrawImage(img, 0, 0);
                        if (centroidPointList != null || centroidPointList.Count > 0)
                        {
                            for (int i = 0; i < centroidPointList.Count; ++i)
                            {
                                Brush brush = new SolidBrush(color[0]);
                                g.FillEllipse(brush, centroidPointList[i].X, centroidPointList[i].Y, 3, 3);
                            }
                        }
                        g.Dispose();
                    }
                }
                else //否則直接操作
                {
                    g = Graphics.FromImage(result);
                    if (centroidPointList != null || centroidPointList.Count > 0)
                    {
                        for (int i = 0; i < centroidPointList.Count; ++i)
                        {
                            Brush brush = new SolidBrush(color[0]);
                            g.FillEllipse(brush, centroidPointList[i].X, centroidPointList[i].Y, 3, 3);
                        }
                    }
                    g.Dispose();
                }
            }
            return result;
        }


        /// <summary>
        /// 會產生graphics異常的PixelFormat
        /// </summary>
        private static PixelFormat[] indexedPixelFormats = { PixelFormat.Undefined, PixelFormat.DontCare,PixelFormat.Format16bppArgb1555,
                                             PixelFormat.Format1bppIndexed, PixelFormat.Format4bppIndexed, PixelFormat.Format8bppIndexed};


        /// <summary>
        /// 判斷圖片的PixelFormat 是否在引發異常的 PixelFormat 之中
        /// </summary>
        /// <param name="imgPixelFormat">原圖片的PixelFormat</param>
        /// <returns></returns>
        private static bool IsPixelFormatIndexed(PixelFormat imgPixelFormat)
        {
            foreach (PixelFormat pf in indexedPixelFormats)
            {
                if (pf.Equals(imgPixelFormat)) return true;
            }
            return false;
        }

具體呼叫方法如下,其中引數openImage應為二值影象。

            int numberOfRuns = 0;
            List<int> stRun = new List<int>(); List<int> enRun = new List<int>(); List<int> rowRun = new List<int>();
            List<Point> stPoint; List<Point> endPoint; List<List<int>> equaList; List<Point> centroidPointList;
            List<int> runLabels = new List<int>(); List<EqualRun> equivalences = new List<EqualRun>();
            curImageProcess.fillRunVectors(openImage, ref numberOfRuns, ref stRun, ref enRun, ref rowRun, out stPoint, out endPoint);
            curImageProcess.firstPass(ref stRun, ref enRun, ref rowRun, ref numberOfRuns, ref runLabels, ref equivalences, 1);
            curImageProcess.replaceSameLabel(ref runLabels, ref equivalences, out equaList);
            curImageProcess.centroidCalculate(ref stPoint, ref endPoint, ref runLabels, out centroidPointList);
            Bitmap resBitmap = curImageProcess.drawCaliResult(openImage, centroidPointList);
結果如下圖(圖片有裁剪):