C# 連通域及其質心標記
阿新 • • 發佈:2018-12-08
連通域在影象處理領域用的還是比較多的,在網上搜索連通域提取的演算法和程式碼,比較多的是用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);
結果如下圖(圖片有裁剪):