效果图如下:
/*
工业视觉_70:多对象轮廓的相位差匹配(旋转无关性匹配)
未来20年里,机器换人,智能使用机器人,改造传统工厂成智慧工厂,将成为最火的行业之一.
工业视觉,目标很明确:"快,准,稳"三个字. 快:开发快,运行速度快;准:高精度;稳:稳健可靠
使用高级语言做工程主要优势在:已经有丰富的数据结构和成熟的类型库,如List,Dictionary,Lambda,Accord,...
所以,目前"机器换人"项目大多采用工控电脑,搭建 Windows7+VS2019+EMGU(或AForge+Accord),这一方案见效最快.
Halcon,Emgu的视觉库都很强大,而AForge+Accord库更全面,更丰富(如:数学,人工智能,机器学习).
许多产品,类似圆,正方形,没有明显的主轴,但是又必须匹配角度的.
在产品的研发中,本人提出了"相位差匹配"思路:
1,提取对象轮廓,按时针方向计算相对于中心的半径集;
2,不同工件的半径集作个移相匹配.找出误差最小时的角度.角度可取(0,1,2,3,...,359)
3,当然,给半径集进行小波分析,或富立叶级数变换,还能意外的发现.
4,计算出了产品相片中的方位角,便于用机器人的姿态变换去拾取与装配.
本文系作者在"安吉八塔机器人公司"产品线研发中的拓展与随笔.
[已经发布,链接: ]
--------- 编撰: 项道德(微信:daode1212),2021-07-21
*/
pictureBox1.Image = Image.FromFile("工业视觉_9.png");
//自定义函数MSG()即MessageBox.Show().
//MSG("马上呈现: 螺旋线搜索法,建立有序边缘点集");
Bitmap bmp = (Bitmap)(pictureBox1.Image);
int ww = bmp.Width, hh = bmp.Height;
bmp = LockBits03(bmp);//自定义函数, 黑区边缘化-->序列化,彩色标注
//本地绘制:
Graphics g = Graphics.FromImage(bmp);
//Pen pen0;
//int clr = 0;
//Point z0 = serialPoints.ElementAt(0).Key;
//foreach (Point z in serialPoints.Keys)
//{
// pen0 = new Pen(Color.FromArgb(clr % 255, clr % 128, clr % 64), 2);
// g.DrawLine(pen0, z0, z);
// clr++; z0 = z;
//}
//分组,取中心,计算半径集,绘制频谱曲线:
var grp = serialPoints.Select(o => o).GroupBy(o=>o.Value);//分组
//Graphics g = Graphics.FromImage(bmp);
SolidBrush bh = new SolidBrush(Color.FromArgb(111, 0, 111));
Pen pen1 = new Pen(Color.FromArgb(222, 0, 222), 1);
Pen pen2 = new Pen(Color.FromArgb(0, 0, 122), 1);
int[] A = new int[4];//长半径所对应的方位角[角度]
//针对各组,计算中心坐标:
int h = 0;
Dictionary<int, List<double>> DL = new Dictionary<int, List<double>>();
foreach (var m in grp)
{
if (m.Count() > 200)
{
float xCenter = (float)m.Average(o => o.Key.X);
float yCenter = (float)m.Average(o => o.Key.Y);
g.FillEllipse(bh, xCenter, yCenter, 8, 8);
g.DrawString("#" + h, new Font("", 10), bh, xCenter - 12, yCenter + 12);
List<double> LD = new List<double>();
float yh = 0;
for (int i = 0; i < m.Count(); i++)
{
float dx = m.ElementAt(i).Key.X - xCenter;
float dy = m.ElementAt(i).Key.Y - yCenter;
float R = (float)Math.Sqrt(dx * dx + dy * dy);
float xh = ww * i / (float)m.Count();
yh = hh / 2 + h * 60 - 70;
g.DrawLine(pen1, xh, yh, xh, yh - R / 3);
LD.Add(R);
}
g.DrawString("#" + h, new Font("", 10), bh, ww / 2, yh - 60);
//画最小半径:
float minR = (float)LD.Min();
for (int i = 0; i < m.Count(); i++)
{
if (LD.ElementAt(i) == minR)
{
float dx = m.ElementAt(i).Key.X;
float dy = m.ElementAt(i).Key.Y;
g.DrawLine(pen1, xCenter, yCenter, dx, dy);
}
}
//画最大半径:
float maxR = (float)LD.Max();
for (int i = 0; i < m.Count(); i++)
{
if (LD.ElementAt(i) == maxR)
{
float dx = m.ElementAt(i).Key.X;
float dy = m.ElementAt(i).Key.Y;
g.DrawLine(pen2, xCenter, yCenter, dx, dy);
A[h] = (int)(toD*Math.Atan2(m.ElementAt(i).Key.Y - yCenter, m.ElementAt(i).Key.X - xCenter));//toD=180/PI
}
}
DL.Add(h, LD);
h++;
}
}
//检查各集合中元素数目:
//string s = string.Format("{0},{1},{2},{3},", DL[0].Count(),DL[1].Count(),DL[2].Count(),DL[3].Count());
//MSG(s);
//全部约束到0,1,2,...,359中:
Dictionary<int, List<double>> DL360 = new Dictionary<int, List<double>>();
foreach(var m in DL.Keys)
{
double[] Pm= DL[m].ToArray();
List<double> Ld = new List<double>();
for (int i=0;i<360;i++)
{
int u=(i*Pm.Length / 360);
Ld.Add(Pm[u]);
//g.DrawLine(pen2, 2*i, 60*m+hh/2, 2*i, 60*m+hh/2+(float)Pm[u]/4);
}
DL360.Add(m,Ld);
}
//找出最大半径所对应的角度,可作相伴偏移计算的起始位置:
string s1 ="Angles for 0,1,2,3: "+ string.Join("|", A);
MSG(s1);//148,80,19,-31, 148-(-31)=179[0:3]接近180度, 80-19=61[1:2]接近60度
//检查各集合中元素数目:
//string s2 = string.Format("{0},{1},{2},{3},", DL360[0].Count(), DL360[1].Count(), DL360[2].Count(), DL360[3].Count());
//MSG(s2);
//=======================原始的相位差(实际应用中应该有统一的起始角度)==============================
//对比DL360[0]与DL360[3],搜索最小误差的角度偏移::
double minStd = int.MaxValue;int uGd = 0;
double[] d0 = DL360[0].ToArray();double[] d3 = DL360[3].ToArray();
for (int u = 0; u < 360; u++)
{
double sm = 0;
for (int i = 0; i < 360; i++)
{
int j =( u + i) % 360;
sm += Math.Abs(d3[j] - d0[i]);
}
if (sm<=minStd)
{
minStd = sm;
uGd = u;
}
}
//MSG(minStd+",0--3 at :"+uGd);
g.DrawString((int)minStd + ", for [0]--[3], AngleDiff=" + uGd, new Font("", 10), bh, ww / 2 - 70, hh - 30);
//对比DL360[1]与DL360[2],搜索最小误差的角度偏移:
minStd = int.MaxValue;
double[] d1 = DL360[1].ToArray();
double[] d2 = DL360[2].ToArray();
for (int u = 0; u < 360; u++)
{
double sm = 0;
for (int i = 0; i < 360; i++)
{
int j =( u + i) % 360;
sm += Math.Abs(d2[j] - d1[i]);
}
if (sm<=minStd)
{
minStd = sm;
uGd = u;
}
}
//MSG(minStd+",1--2 at :"+uGd);
g.DrawString( (int)minStd +", for [1]--[2], AngleDiff=" + uGd, new Font("", 10), bh, ww / 2-70, hh-50);
pictureBox1.Image = bmp;
//自定义函数LockBits03代码如下:
/// <summary>
/// 边缘化与彩色分类,各类中:边缘序列化,从少绿到多绿.
/// </summary>
/// <param name="bmp">传入位图</param>
/// <returns>彩色边缘位图</returns>
private Bitmap LockBits03(Bitmap bmp)
{
Bitmap srcBmp = bmp;// new Bitmap(bmp.Width, bmp.Height);
edgePoints.Clear(); serialPoints.Clear();
Bitmap dstBmp = new Bitmap(bmp.Width,bmp.Height);
//1,生成边缘点集edgePoints:
int k = 0;
for (int xi = 2; xi < bmp.Width - 2; xi += 2)
{
for (int yi = 2; yi < bmp.Height - 2; yi += 2)
{
if (Math.Abs(srcBmp.GetPixel(xi - 1, yi).G - srcBmp.GetPixel(xi + 1, yi).G) > 127
|| Math.Abs(srcBmp.GetPixel(xi, yi - 1).G - srcBmp.GetPixel(xi, yi + 1).G) > 127)
{
edgePoints.Add(new Point(xi, yi), k);
}
}
}
//2,生成序列化点集:
int x0 =edgePoints.ElementAt(0).Key.X , y0 =edgePoints.ElementAt(0).Key.Y;//起点
int r = 1; //搜索距离
int m = 0; //分类标志
int x, y;//当前搜索点坐标
while (edgePoints.Count > 0)
{
if (!serialPoints.ContainsKey(new Point(x0, y0))) serialPoints.Add(new Point(x0, y0), m);
edgePoints.Remove(new Point(x0, y0));
//1,上线,向右:
y = y0 - r;
for (x = x0 - r; x < x0 + r; x++)
{
if (edgePoints.ContainsKey(new Point(x, y)))
{
x0 = x; y0 = y;
r = 0; break;
}
}
//2,右线,向下:
x = x0 + r;
for (y = y0 - r; y < y0 + r; y++)
{
if (edgePoints.ContainsKey(new Point(x, y)))
{
x0 = x; y0 = y;
r = 0; break;
}
}
//3,下线,向左:
y = y0 + r;
for (x = x0 + r; x >= x0 - r; x--)
{
if (edgePoints.ContainsKey(new Point(x, y)))
{
x0 = x; y0 = y;
r = 0; break;
}
}
//4,左线,向上:
x = x0 - r;
for (y = y0 + r; y >= y0 - r ; y--)
{
if (edgePoints.ContainsKey(new Point(x, y)))
{
x0 = x; y0 = y;
r = 0; break;
}
}
if (r > 20)
{
m++;
}
r++;
if (r > srcBmp.Width *.5) break;
}
//3,显示:
string txt = "";
txt += "serialPoints.Values.Max=" + serialPoints.Values.Max();
txt += "\r\nedgePoints.Count=" + edgePoints.Count + ", serialPoints.Count=" + serialPoints.Count;
int clr = 0;//绿色成分从少到多
foreach (Point z in serialPoints.Keys)
{
dstBmp.SetPixel(z.X + 1, z.Y, Color.FromArgb((17 * serialPoints[z]) % 255, (clr) % 255, (31 * serialPoints[z]) % 255));
dstBmp.SetPixel(z.X - 1, z.Y, Color.FromArgb((17 * serialPoints[z]) % 255, (clr) % 255, (31 * serialPoints[z]) % 255));
dstBmp.SetPixel(z.X, z.Y - 1, Color.FromArgb((17 * serialPoints[z]) % 255, (clr) % 255, (31 * serialPoints[z]) % 255));
dstBmp.SetPixel(z.X, z.Y + 1, Color.FromArgb((17 * serialPoints[z]) % 255, (clr) % 255, (31 * serialPoints[z]) % 255));
dstBmp.SetPixel(z.X, z.Y, Color.FromArgb((17 * serialPoints[z]) % 255, (clr) % 255, (31 * serialPoints[z]) % 255));
clr++;
}
int h = 0;
for (int z = 0; z <= m; z++)
{
int c = serialPoints.Where(o => o.Value == z).Count();
if (c > 400)
{
txt += "\r\nID:" + z + ",\tCount=" + c + "\t h=" + h;
h++;
}
}
this.textBox1.Text = txt;
return dstBmp;
}