本小游戏类似于4399中的火柴人打羽毛球游戏,在背景图上,左右双方通过键盘来进行接球操作。
实现方法比较简单,其中的知识点包括霍夫圆检测,键盘事件响应,背景填充等。
做这个小游戏的目的主要是为了用于OpenCV学习的阶段性展示。
演示视频如下所示:
basketBallGame
为了增强代码的可读性以及后续对程序的修改,我们对项目中的一些重要对象定义结构体。
包括:游戏角色、背景图、篮球。
// 定义篮球的结构体(把篮球看作圆形。x,y为篮球位置;d为篮球直径)
typedef struct ballLoc {
double x;
double y;
double d;
};
// 人物结构体(人物的高度,宽度,位置)
typedef struct kun {
double heigth;
double width;
double x;
double y;
};
// 背景图结构体(背景图的宽度,高度)
typedef struct backgroundI {
int bg_width;
int bg_height;
};
篮球如下图所示,为了让篮球能够完美地贴合到背景图中,我们这里的先使用霍夫圆检测,把篮球单独从图像之中抠出来备用。
篮球处理分为以下步骤:
1. 预处理(灰度化、高斯模糊)
2. 霍夫圆检测
3. 在检测出来的圆中根据特征提取出我们需要的圆
4. 将提取出来的圆单独定义为一张图像,调整大小并返回给主函数
实现代码如下:
// 篮球处理
Mat ballDetect(Mat image) {
// 1. 预处理(灰度化,滤波)
Mat gray_image,gauss_image;
cvtColor(image, gray_image, COLOR_BGR2GRAY);
GaussianBlur(gray_image, gauss_image, Size(3, 3), 0, 0);
// 2. 霍夫圆检测
// 圆的表示:前两个参数为圆心坐标,第三个参数为半径
vector<Vec3f> ball_circles;
HoughCircles(gauss_image, ball_circles, HOUGH_GRADIENT, 1, 50, 80, 80, 50, 200);
// 3. 找到需要的篮球的圆(最大的圆)
Vec3f ball_circle = ball_circles[0];
for (int i = 0; i < ball_circles.size(); i++)
{
if (ball_circles[i][2] > ball_circle[2]) {
ball_circle = ball_circles[i];
}
}
// 4. 这里的(x,y)为通过圆参数计算出来的正方形的左上角坐标,d为正方形的边长
double x = ball_circle[0] - ball_circle[2];
double y = ball_circle[1] - ball_circle[2];
double d = ball_circle[2]*2;
Rect rect(x, y,d ,d);
// 单独把篮球提取出成一张图像
Mat det_ball;
image(rect).copyTo(det_ball);
resize(det_ball, det_ball, Size(ball_01.d, ball_01.d));
return det_ball;
}
如果使用常规的ball(ROI).copyTo(background(ROI)); 需要把感兴趣区域设置为圆形,但是由于没有看懂这块是怎么用的(我不会!)。所以使用数学的方法暴力进行背景填充。
如下图所示,给出一个圆形和他的外接正方形(正方形边长与圆直径相同),将中心视作图像的原点,那么我们就可以通过判断 x,y的坐标与圆半径r的关系得出这个点是否落在圆内。
但是由于图形的默认坐标是从左上角算起。即 (0,0)点 为图像的左上点。所以我们在遍历图像像素点的过程中应当使用额外的参数来进行判断。
实现代码如下:
圆形区域(篮球)像素点不变;圆形区域外的像素用背景图的对应像素进行替换
// 画球(主要用来分离背景)
// 有坐标位置,有背景图
void drawBall(Mat & ball) {
int r = ball_01.d / 2;
for (int i = -r, ix = 0; i < ball_01.d - r; i++, ix++) {
for (int j = -r, iy = 0; j < ball_01.d - r; j++, iy++) {
if (i * i + j * j > r * r) {
for (int c = 0; c < 3; c++) {
ball.at<Vec3b>(iy, ix)[c] = background.at<Vec3b>(ball_01.y + iy, ball_01.x + ix)[c];
}
}
}
}
}
在主函数中通过while(1){} ,不断循环绘制篮球(通过篮球坐标),由于我们定义了篮球位置的结构体,所以如果想要改变篮球的位置,修改其坐标即可。
// x向着左边越界
if (ball_01.x <= 5) {
ball_01.x += 1;
b_x = -b_x;
}
// x右边界越界
else if (ball_01.x >= (bg_width - ball_01.d - 4)) {
ball_01.x -= 1;
b_x = -b_x;
}
// 上边界
else if (ball_01.y <= 5) {
ball_01.y += 1;
b_y = -b_y;
}
// 下边界
else if (ball_01.y >= (bg_height - ball_01.d - 4)) {
ball_01.y -= 1;
b_y = -b_y;
break;
}
角色与篮球碰撞后,篮球的运动方向改变,且角色状态改变。
这里只是通过篮球和角色的位置以及他们各自的高度宽度进行检测,因此如果当篮球的大小设置的过于大时,会出现bug,感觉后续可以通过opencv视觉的方法进行替换。代码如下:
// 检测哥哥与篮球碰撞
if (((ball_01.y + ball_01.d) >= kun01.y &&
(ball_01.x + ball_01.d) >= kun01.x &&
(ball_01.x + ball_01.d) <= (kun01.x + kun01.width))
||
((ball_01.y + ball_01.d) >= kun_right.y &&
(ball_01.x + ball_01.d) >= kun_right.x &&
(ball_01.x + ball_01.d) <= (kun_right.x + kun_right.width)
))
{
drawKun(background, kunImage2, 0);
drawKun(background, kunImage2, 1);
b_y = -b_y;
}
角色移动通过监听键盘响应实现。
使用 int key = waitKeyEx(1); 获取到按键,再通过按键内容进行响应。
int key = waitKeyEx(1);
if (key == 2424832)
{
//键盘←键
if (kun01.x > 10) {
kun01.x -= 10;
drawKun(background, kunImage2, 0);
}
}
if (key == 2555904)
{
//键盘→键
if (kun01.x < ((bg01.bg_width - 10 - kun01.width)/2)) {
kun01.x += 10;
drawKun(background, kunImage2, 0);
}
}
if (key == 97)
{
//键盘a键
if (kun_right.x > ((bg01.bg_width - 10 - kun_right.width)/2)) {
kun_right.x -= 10;
drawKun(background, kunImage2, 1);
}
}
if (key == 100)
{
//键盘d键
if (kun_right.x < (bg01.bg_width - 10 - kun_right.width)) {
kun_right.x += 10;
drawKun(background, kunImage2, 1);
}
}
vector<Vec3f> ball_circles;
HoughCircles(gauss_image, ball_circles, HOUGH_GRADIENT, 1, 50, 80, 80, 50, 200);
int key = waitKeyEx(1);
key = 100:d
key = 97:a
key = 2555904:键盘→键
key = 2424832:键盘←键
可以尝试用数学的角度去解决一些图像中的问题。
例如正方形中最大圆的检测以及碰撞中球的运动轨迹变化
https://download.csdn.net/download/weixin_46221106/87212956