激光制导的井字游戏, 使用摄像头进行视觉识别






4.97/5 (51投票s)
在本文中,我们将一起构建一个程序,它将允许我们使用激光灯和网络摄像头进行视觉,与计算机玩井字棋游戏。

引言
图像处理和游戏编程可能是程序员可能拥有的最具技术挑战性的工作。顶级程序可能需要程序员和计算机的最好的。井字棋是一个简单的游戏,它使构建算法来让计算机玩自己的动作变得容易。在本文中,开发了一个基于网络摄像头的交互式井字棋游戏。该程序使用 C# .NET 编写,使用 AForge .NET Framework 执行某些图像处理任务和图像采集。
应用程序中的网络摄像头
第一步是在我们的程序中从网络摄像头获取视频源。我们可以使用 AForge .NET 框架来完成此操作。使用 AForge .NET Framework 进行图像采集既简单又快速。查看下面的代码片段
private void getCamList()
{
try
{
videoDevices = new FilterInfoCollection(FilterCategory.VideoInputDevice);
comboBox1.Items.Clear();
if (videoDevices.Count == 0)
throw new ApplicationException();
DeviceExist = true;
foreach (FilterInfo device in videoDevices)
{
comboBox1.Items.Add(device.Name);
}
comboBox1.SelectedIndex = 0; //make default to first cam
}
catch (ApplicationException)
{
DeviceExist = false;
comboBox1.Items.Add("No capture device on your system");
}
}
//refresh button
private void rfsh_Click(object sender, EventArgs e)
{
getCamList();
}
//toggle start and stop button
private void start_Click(object sender, EventArgs e)
{
if (start.Text == "&Start")
{
if (DeviceExist)
{
videoSource =
new VideoCaptureDevice(videoDevices[comboBox1.SelectedIndex].MonikerString);
videoSource.NewFrame += new NewFrameEventHandler(video_NewFrame);
CloseVideoSource();
//videoSource.DesiredFrameRate = 10;
videoSource.Start();
label2.Text = "Device running...";
start.Text = "&Stop";
}
else
{
label2.Text = "Error: No Device selected.";
}
}
else
{
if (videoSource.IsRunning)
{
timer1.Enabled = false;
CloseVideoSource();
label2.Text = "Device stopped.";
start.Text = "&Start";
}
}
}
//eventhandler if new frame is ready
private void video_NewFrame(object sender, NewFrameEventArgs eventArgs)
{
Bitmap img = (Bitmap)eventArgs.Frame.Clone();
// All the image processing is done here...
pictureBox1.Image = img;
}
//close the device safely
private void CloseVideoSource()
{
if (!(videoSource == null))
if (videoSource.IsRunning)
{
videoSource.SignalToStop();
videoSource = null;
}
}
将视频想象成一个连续的图像流,对于实时图像处理,我们需要处理网络摄像头捕获的每一张图像。我的应用程序中平均每秒 7 帧。帧率越高,您的程序运行就越流畅。
设计和 GUI
正如您在上面的图片中看到的,我使用 C# .NET 的 GDI+ 为井字棋创建了一个非常基本的设计。
激光检测和游戏玩法
该程序搜索网络摄像头视野中最亮的红色斑点。我们可以通过使用 AForge .NET Framework 中定义的两个过滤器来做到这一点。
- 颜色过滤器 过滤掉除红激光光之外的所有内容。
- 斑点计数器 检索图像中激光点或红色斑点的位置。
ColorFiltering filter = new ColorFiltering(); // define a color filter
// Define the range of RGB to retain within a processed image
filter.Red = new IntRange(254, 255);
filter.Green = new IntRange(0, 240);
filter.Blue = new IntRange(0, 240);
Bitmap tmp = filter.Apply(image); // apply the color filter to image
IFilter grayscale = new GrayscaleBT709();// define grayscale filter
// b'coz blobcounter supports grayscale 8 bpp indexed
tmp = grayscale.Apply(tmp); // applying the filter
// locking the bitmap data
BitmapData bitmapData = tmp.LockBits(new Rectangle(0, 0, image.Width, image.Height),
ImageLockMode.ReadWrite, PixelFormat.Format8bppIndexed);
blobCounter blobCounter = new BlobCounter(); // define an object of blobcounter class
blobCounter.ProcessImage(bitmapData); // process the locked bitmap data to find blobs
blobCounter.ObjectsOrder = ObjectsOrder.Size; // arrange the blob in increasing size
Rectangle[] rects = blobCounter.GetObjectsRectangles(); //get the rectangle out a blob
tmp.UnlockBits(bitmapData);
tmp.Dispose();
if (rects.Length != 0)
{
backpoint = GetCenter(rects[0]); // getcenter is a function to find
// the center of any rectangle ( this is position of laser light)
DrawLines(ref image, backpoint); // draw horizontal and vertical line on a
// center point (see the picture above)
}
检测到激光后,程序会跟踪激光点的移动,一旦激光关闭,它就会在该网格中放置用户的动作。为了理解这一点,请想象一个井字棋棋盘,上面有九个不同的网格,如下图所示

该算法的设计方式是将网络摄像头的每一帧分成九个相等的矩形,描绘井字棋游戏的九个不同网格。用户动作放置在关闭激光灯的网格中。如果您没有激光灯或者您无法可视化此程序,请查看 YouTube 上的视频。为了优化代码,我们可以在进行任何移动之前添加更多过滤器,例如检查网格是否为空且未被十字或零占据,我们还可以添加一些暂停或睡眠,以便在程序确认网络摄像头视图中没有激光灯之前。
井字棋算法
我尽我所能实现了 AI,即使代码很长。算法的设计采用了人性化的方法来玩井字棋。它首先尝试赢得游戏,阻止其他玩家获胜,然后尝试进行移动,避免“陷阱”并将两个计算机棋子放在一行中。如果这些都不可能,则进行随机移动。
using System;
namespace WebcamTicTacToe
{
class ZeroCross
{
public bool winner_comp = false; // whether winner is computer
public bool winner_you = false;// whether winner is user
public bool tie = false;// game goes in tie
private int[] zeros = { 0, 0, 0, 0, 0 };// stores moved played by user
private int[] crosses = { 0, 0, 0, 0, 0 };//stores moves played by computer
private int countero;//count the zeros played
private int counterx;//count crosses played
public int MakeAMove(int MyMove)//This function is called to make
//a move by computer and takes user move as argument
{
zeros[countero] = MyMove; // stores the user move in member function
countero++;
if (counterx == 0) // if there is no crosses played make a random move
{
int rand_move = RandomMove();
crosses[counterx] = rand_move;
counterx++;
return rand_move;
}
else
{
// this will try to win the game at first,
// preventing the other player to win, and then tries to
// make a move avoiding "traps" and putting
// two computer pieces in a row
//If none of this is possible, a random move is made.
int[,] Pattern = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 },
{ 1, 4, 7 }, { 2, 5, 8 }, { 3, 6, 9 }, { 1, 5, 9 }, { 7, 5, 3 } };
int Compmove = 0;
int FinalMove = 0;
int winning_Move = 0;
for (int i = 0; i < 8; i++)
{
int oCounter = 0;
int xCounter = 0;
int Incase = 0;
for (int j = 0; j < 3; j++)
{
if (CrossSeek(Pattern[i, j]))
{
xCounter++;
}
else if (ZeroSeek(Pattern[i, j]))
{
oCounter++;
}
else
{
Incase = Pattern[i, j];
}
}
if (oCounter == 3)
{
winner_you = true;
return 0;
}
else if (xCounter == 2 && Incase != 0)
{
winning_Move = Incase;
}
else if (oCounter == 2 && Incase != 0)
{
Compmove = Incase;
}
else if (oCounter == 0 && xCounter == 1)
{
if (Compmove == 0)
{
Compmove = Incase;
}
}
else if (Incase != 0)
{
FinalMove = Incase;
}
}
if (winning_Move != 0)
{
crosses[counterx] = winning_Move;
counterx++;
winner_comp = true;
return winning_Move;
}
if (Compmove != 0)
{
crosses[counterx] = Compmove;
counterx++;
if (counterx == 5 || countero == 5)
{
winner_you = false;
winner_comp = false;
tie = true;
}
return Compmove;
}
if (FinalMove != 0)
{
crosses[counterx] = FinalMove;
counterx++;
if (counterx == 5 || countero == 5)
{
tie = true;
}
return FinalMove;
}
tie = true;
return 0;
}
}
// In case computer play first move
public int CompFirstTurn()
{
int CompMove = RandomMove();
crosses[counterx] = CompMove;
counterx++;
return CompMove;
}
// get winner is called at end to find the winner
public bool GetWinner()
{
if (winner_comp || winner_you || tie)
{
return true;
}
return false;
}
// this searches whether any zero is placed at position passed in argument
public bool ZeroSeek(int zero)
{
for (int i = 0; i < countero; i++)
{
if (zeros[i] == zero)
{
return true;
}
}
return false;
}
// this searches whether any cross is placed at position passed in argument
public bool CrossSeek(int cross)
{
for (int i = 0; i < counterx; i++)
{
if (crosses[i] == cross)
{
return true;
}
}
return false;
}
//make a random move
private int RandomMove()
{
Random random = new Random();
int CompMove;
bool truth = false;
do
{
CompMove = random.Next(1, 10);
if (zeros[0] == CompMove || crosses[0] == CompMove)
{
truth = true;
}
else
{
truth = false;
}
}
while (truth);
return CompMove;
}
}
}
使用程序
嗯,该程序需要激光灯和网络摄像头以 640 X 420 的分辨率流式传输图像,您可以修改本文中提供的源代码以匹配您网络摄像头的分辨率。
我已经在游戏中添加了 AI,即使您的机器不是高端机器并且它以低帧速率流式传输图像,也能实现流畅的游戏玩法。该程序将尝试预测您最有可能的移动,即使您的激光笔靠近它,也会自动放置它。
结论
我们已经到达本文的结尾。我可能会更新此程序以添加更多优化以获得最佳游戏体验。但是,现在,玩得开心!您可以在我的 博客 上找到此应用程序的一些视频。您还可以轻松修改代码,使该程序做更多的事情,而不仅仅是玩井字棋。玩得开心!历史
- 2009 年 12 月 1 日:初始发布