我正在做一个小项目,需要从给定的纸质靶标图像中检测得分情况。这类似于iPhone上的TargetScan应用程序。
我使用OpenCV来处理图像,项目分为两个部分:一部分是检测靶标上的圆圈(使用霍夫圆变换效果很好),另一部分是检测射击点。我需要一些关于如何从给定图像中检测这些射击点的主意。以下是一个带有圆圈检测的示例图像(绿色线条表示检测到的圆圈,红色点表示中心)。OpenCV中可以使用哪些算法来检测这些射击点?
这里是另一个示例图像
回答:
算法:
- 创建/清除图像的掩码
- 二值化图像(根据某个强度阈值转换为黑白)
- 处理所有像素
-
计算
x,y
方向上相同颜色的像素数量称之为
wx,wy
-
检测圆圈、射击点和中间部分
圆圈较细,因此
wx
或wy
应小于细阈值,而另一个应较大。射击点较大,因此wx
和wy
都必须在射击点直径范围内。中间部分是黑色的,且wx,wy
都超过所有阈值(可以在这里计算平均点)。将这些信息存储到掩码中 -
使用掩码信息重新着色图像
-
从找到的点计算圆圈的中心和半径
中心是中间部分区域的平均点,现在处理所有绿色点并计算其半径。对所有找到的半径进行直方图统计,并按计数降序排序。计数应与
2*PI*r
一致,否则忽略此类点。 -
将射击点像素分组
因此,进行分割或泛洪填充重新着色每个命中点,以避免对单个射击点进行多次计数
我用C++编写了#1..#6部分的代码,供娱乐使用,代码如下:
picture pic0,pic1,pic2; // pic0 - 源图像 // pic1 - 输出图像 // pic2 - 掩码 int x,y,i,n,wx,wy; int r0=3; // 细曲线宽度阈值 [像素] int r1a=15; // 射击点直径最小阈值 [像素] int r1b=30; // 射击点直径最大阈值 [像素] int x0,y0; // 平均点 == 中心 // 初始化输出图像为源图像,但仅保留灰度强度 pic1=pic0; pic1.rgb2i(); // 初始化掩码(与源图像大小相同) pic2.resize(pic0.xs,pic0.ys); pic2.clear(0); // 二值化图像并转换回RGB for (y=r0;y<pic1.ys-r0-1;y++) for (x=r0;x<pic1.xs-r0-1;x++) if (pic1.p[y][x].dd<=500) // 黑/白阈值 <0,765> pic1.p[y][x].dd=0x00000000; // RGB中的黑色 else pic1.p[y][x].dd=0x00FFFFFF; // RGB中的白色 // 处理像素 x0=0; y0=0; n=0; for (y=r1b;y<pic1.ys-r1b-1;y++) for (x=r1b;x<pic1.xs-r1b-1;x++) { wy=1; // 计算列中相同颜色的像素数量 for (i=1;i<=r1b;i++) if (pic1.p[y-i][x].dd==pic1.p[y][x].dd) wy++; else break; for (i=1;i<=r1b;i++) if (pic1.p[y+i][x].dd==pic1.p[y][x].dd) wy++; else break; wx=1; // 计算行中相同颜色的像素数量 for (i=1;i<=r1b;i++) if (pic1.p[y][x-i].dd==pic1.p[y][x].dd) wx++; else break; for (i=1;i<=r1b;i++) if (pic1.p[y][x+i].dd==pic1.p[y][x].dd) wx++; else break; if ((wx<r0)||(wy<r0)) // 如果是细的 if ((wx>=r0)||(wy>=r0)) // 但仍然是线 { pic2.p[y][x].dd=1; // 细线 } if (pic1.p[y][x].dd==0) // 黑色 if ((wx>=r0)&&(wy>=r0)) // 且在两个轴上都较厚 { pic2.p[y][x].dd=2; // 中间部分 x0+=x; y0+=y; n++; } if (pic1.p[y][x].dd) // 白色(背景颜色) if ((wx>r1a)&&(wy>r1a)) // 大小在射击点范围内 if ((wx<r1b)&&(wy<r1b)) { pic2.p[y][x].dd=3; // 射击点 } } if (n) { x0/=n; y0/=n; } // 将掩码数据(重新着色)添加到输出图像中// if (0) for (y=0;y<pic1.ys;y++) for (x=0;x<pic1.xs;x++) { if (pic2.p[y][x].dd==1) pic1.p[y][x].dd=0x0000FF00; // 绿色细线 if (pic2.p[y][x].dd==2) pic1.p[y][x].dd=0x000000FF; // 蓝色中间部分 if (pic2.p[y][x].dd==3) pic1.p[y][x].dd=0x00FF0000; // 红色射击点 } // 中心十字线 i=25; pic1.bmp->Canvas->Pen->Color=0x0000FF; pic1.bmp->Canvas->MoveTo(x0-i,y0); pic1.bmp->Canvas->LineTo(x0+i,y0); pic1.bmp->Canvas->MoveTo(x0,y0-i); pic1.bmp->Canvas->LineTo(x0,y0+i);
我使用自己的picture类来处理图像,因此一些成员如下:
xs,ys
图像大小(以像素为单位)p[y][x].dd
是(x,y)
位置的像素,以32位整数类型表示clear(color)
– 清除整个图像resize(xs,ys)
– 调整图像到新的分辨率
这是重新着色后的结果
- 绿色 – 细圆圈
- 蓝色 – 中间部分
- 红色十字 – 圆圈中心
- 红色 – 射击点
如您所见,它需要进一步处理第#7、#8步的操作,并且您的图像中没有中间部分以外的射击点,因此可能还需要调整以检测中间部分以外的射击点
[edit1] 半径
// 创建并清除半径直方图n=xs; if (n<ys) n=ys;int *hist=new int[n];for (i=0;i<n;i++) hist[i]=0;// 计算直方图for (y=0;y<pic2.ys;y++) for (x=0;x<pic2.xs;x++) if (pic2.p[y][x].dd==1) // 细像素 { i=sqrt(((x-x0)*(x-x0))+((y-y0)*(y-y0))); hist[i]++; }// 合并邻近半径for (i=0;i<n;i++) if (hist[i]) { for (x=i;x<n;x++) if (!hist[x]) break; for (wx=0,y=i;y<x;y++) { wx+=hist[y]; hist[y]=0; } hist[(i+x-1)>>1]=wx; i=x-1; }// 绘制有效圆圈pic1.bmp->Canvas->Pen->Color=0xFF00FF; // 品红色pic1.bmp->Canvas->Pen->Width=r0;pic1.bmp->Canvas->Brush->Style=bsClear;for (i=0;i<n;i++) if (hist[i]) { float a=float(hist[i])/(2.0*M_PI*float(i)); if ((a>=0.3)&&(a<=2.1)) pic1.bmp->Canvas->Ellipse(x0-i,y0-i,x0+i,y0+i); }pic1.bmp->Canvas->Brush->Style=bsSolid;pic1.bmp->Canvas->Pen->Width=1;delete[] hist;
检测到的圆圈是品红色…我觉得效果不错。中间部分稍微影响了一些结果。您可以计算平均半径步长并插值缺失的圆圈…