自动炮台
本帖最后由 Lee 于 2014-4-27 17:48 编辑摘自:http://open1book.com/http://weibo.com/openebookOpenbook 开源杂志第6、8期.Openbook是是一群志愿者自发组织起来推广geek文化的开源期刊,她包罗万象,深入浅出,友善亲切,纵横捭阖。Openbook向各行各业的创客征稿中,详情可以加群1277738咨询。
特别感谢(排名不分先后):Arduino.cn ; Openjumper; Openbook;以及神一样的QQ群:1277738
任何形式的转载都请标明出自“Openbook 开源杂志,http://open1book.com/”
作者:Lee苦海 作者简介:Q技能是忽悠,W技能是英语翻译,E技能是创客,大绝是制作一个微焦的冰淇淋蛋糕。被动技能是一天只睡5小时。
(本文太长了 难免有各种疏漏 如发现错漏请私信我)
(本文中炮台使用的Nerf子弹与知名玩具厂商孩之宝使用的玩具子弹相同,适用年龄为6岁以上;因为有焊接和机械,请18岁以下或者没有工程经验的小伙伴千万小心;本设计只为了帮组一些参加正规机器人比赛的同学,其他任何形式的仿制均不管我事)
由于最近各种机器人比赛都开始有射击这一项目,作者参加的两个赛也都有这个项目,所以做个自动炮塔是免不了了。本文针对日间或室内的环境要求,采用计算机视觉瞄准,配上机械和电子系统控制扳机和瞄准角度,基本上完成了这个要求。
第一部分:炮台(云台)日常我们使用的电机有3种,直流,步进,和舵机。由于直流电机需要闭环控制才能实现其他两种电机不闭环就能实现的功能,所以考虑到系统的复杂程度直接放弃了。步进电机直接通过给予信号序列来控制其转动,虽说给信号比起直流电机和舵机纯PWM驱动要复杂一点,但是考虑到他的控制精度,不失为一个解决方法。舵机就是最简单的办法,淘宝上有大量2自由度云台可选,短时间内求成果的推荐直接舵机。我自己一共有3套云台,个头目的各不相同,第一套用的两个28BYJ-48步进,第二套用的Servocity的 SPT200 Pan & Tile System云台。第三套用的两只57步进。
第一套系统:这套系统缺点是实在带不了什么大家伙,完全用电机轴在做支撑,那四个开关也没有给多少转动的空间。
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image002.jpgfile:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image004.jpg file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image006.jpg
第二套系统:购自http://www.servocity.com/html/spt200_pan___tilt_system.html。 这套系统也没法带多大的家伙,网站说最大两磅,换句话就是一公斤不到。但是目前多数比赛只是要求带NERF GUN而已,所以这套已经能够满足一般比赛要求。
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image008.jpg
第三套系统:由作者队友做的, 两个57步进控制转动,另加了轴承,链条等,虽说不是很好看,但是带5公斤不是大问题。
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image010.jpgfile:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image012.jpg程序流程:
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image014.jpg第二部分:图像识别大家要瞄人瞄车的就别看了,本文超级和谐的。目前大多数机器人打靶都停留在用特定颜色做靶子的状态下,比如国际工程师协会米帝东南区大学生智能小车赛就是使用一个红色的木板,中间钻一个洞来做目标。由此可见,一个使用摄像有做颜色识别的程序就能完成这个要求。图像处理使用OPENCV 2.4版本,想做的童鞋们可以从此处下载http://opencv.org/。为了减少不必要的麻烦,作者使用的就是一般的USB摄像头而已。如果你已经下载并安装好了OPENCV,不妨开始复制粘贴。#include <cv.h>#include <highgui.h>using namespace cv;using namespace std;需要使用的库和一些为了方便的简化工作。
VideoCapture cap(0);调用0号摄像头。只要这一句,不用在乎你插在哪个USB口,OPENCV全部搞定。请注意,OPENCV目前只支持到2个摄像头,如果需要使用超过2个摄像头,需要使用其他的库来调用它们,用OPENCV调用会导致错误。
Mat img;Cap>> img;创建一个叫”img”的矩阵类,并把当前帧的图像存进去。Mat是OPENCV的常用类之一,他的好处实在太多,比如这里作者完全没考虑他的当前大小直接就把图像给塞进去。
imshow( "cam0 ", img );建立一个叫cam0的窗口并在该窗口显示“img”保存的图像。这里如果你已经有一个同名的窗口,则会在同名的窗口中显示。 窗口大小会自动调节。
Mat HSV;cvtColor(img, HSV, CV_BGR2HSV );创建一个叫“HSV”的矩阵类,并把颜色空间转换的结果存进去。这里使用的是HSV色彩空间,即色相,饱和度,明度。一般我们使用的相机,摄像头都是使用RGB色彩空间,即红,绿,蓝。不使用RGB色彩空间而使用HSV色彩空间为为了更好的提出一个颜色。 如图:(Hue,Saturation, Value分别是色相,饱和度,明度)
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image016.jpg file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image018.jpg
从第二个图可以清楚的看出,如果目标是绿色的,只需要在H区间取值80-160度即可。红色较麻烦,需要取值两次,一次是从0到20度,另一次是从320到360度。(多少有点误差,各位根据具体情况具体修正)而如果使用RGB色彩空间,各颜色的分布就比较复杂,比较困难一次抓出所有的红色。如图,红色的分布是在以R=255,B=0,G=0为中心的球内,这无疑给挑取颜色增加很多难度。file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image020.jpg
Mat thresh; inRange(YCrCb, Scalar(pos1, pos2, pos3), Scalar(pos4, pos5, pos6), thresh);//color建立一个叫”thresh”的矩阵类,选取全部满足pos1>H>pos4,pos2>S>pos4,pos3>S>pos6的像素,并在thresh用1表示。其余未被选取的像素用0表示。这部操作叫二值化。
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image022.jpg file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image024.jpg
我以那本书为假设目标,这是抓取结果:
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image025.pngfile:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image026.png
dilate( thresh, thresh,element,Point(-1, -1), pos7);erode ( thresh, thresh,element,Point(-1, -1),pos8 );对thresh进去扩散和腐蚀,并将结果存入thresh内。这个过程可以有效的减少椒盐噪声和轮廓复杂度。Pos7为扩散次数,pos8为腐蚀次数。扩散1-3次结果:从图可以看出,书上的字就被我直接处理掉了。
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image028.png file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image029.png腐蚀4,8次结果: file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image031.png作者为了得到完整的书,先使用了20次扩散把书上的各个洞洞全部补掉,然后25次腐蚀把背景中的噪声去掉,最后5次扩散把书的像素大小还原。这是结果file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image033.jpg上回我们说到从摄像头的输出中把目标的颜色提出来,并使用扩散和腐蚀效果对结果做一点加工。这叫二值化。下一步就是根据目标颜色在图像中位置,找出目标的中心。
imshow( "thresh", thresh,); //和之前一样,我们可以使用imshow()来查看效果,之后也一样,只要对象是mat或Iplimage(本文未使用)格式,都可以使用imshow()来查看。
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image035.jpg当然对于人而言,目标已经很清晰了。但是对计算机来说,计算机看到的还是一堆数字,它并不知道目标的位置。所以我们使用cvFindContours()来让计算机找出目标的轮廓。
vector<vector<Point> >contours; vector<Vec4i> hierarchy; findContours( thresh, Rcontours,Rhierarchy, CV_RETR_TREE, CV_CHAIN_APPROX_SIMPLE, Point(0, 0) );//这个函数会找出图像中的轮廓,公式有点长,但多数时侯都不会做任何改写。照抄即可。
在理想情况下,我们通常可以用上期提到的扩散和腐蚀来去掉画面多数不想要的噪声。但是有些噪声可能比较顽固,这里我们可以检查全部轮廓,从中找出最大的轮廓将其当成目标,并把其他轮廓全部当成噪声。
vector<vector<Point>> contours_poly( contours.size() ); vector<Rect> boundRect(contours.size() ); //定义一个Rect矢量来存放轮廓。因为轮廓的外形多数时候是不规则的。所以用一个矩形来代替不规则的轮廓会在各种方面都方便很多。 vector<Point2f>center(contours.size() ); vector<float>radius(contours.size() );
intmidX=0; int midY=0; int maxArea =0; int index=0; for( unsigned int i = 0; i<contours.size(); i++ ) )//用一个for循环语句查看计算机找到的全部轮廓 { int area=contourArea(contours);// 计算当前轮廓的包含面积 if(area> maxArea)//找出包含面积最大的轮廓 { maxArea = area; index =i; } approxPolyDP( Mat(contours),contours_poly, 3, true );// approxPolyDP()用来找出轮廓的近似多边形。用于简化轮廓的复杂度,加速计算过程。 boundRect = boundingRect(Mat(contours_poly) );//BoundingRect()是一个用来找出轮廓最小包围矩形函数。最小包围矩形的意思就是用4条边从上下左右四个方向把轮廓紧紧夹在中间。这4条边构成的矩形就是最小包围矩形。 minEnclosingCircle((Mat)contours_poly, center, radius );// minEnclosingCircle()是一个用来找出轮廓最小包围圆形的函数。其原理类似最小包围矩形。如果目标是圆形时,找最小包围圆比最小包围方更方便 drawContours( frame, contours,index, red, 1, 8, hierarchy, 0, Point() );//画出物体的轮廓 rectangle( frame,boundRect.tl(), boundRect.br(),red, 2, 8, 0 );//画出物体的最小包围矩形 circle( frame, center,(int)Rradius, red, 2, 8, 0 );//画出物体的最小包围圆形
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image037.jpg//图中有三个框框框住了红宝书,近贴着红宝书的是approxPolyDP()算出的轮廓。//矩形的自然就是boundRect()算出的轮廓。//圆形的自然就是minEnclosingCircle()算出的轮廓。 //计算目标的中心坐标 midX = RboundRect.width/2 +boundRect.x; midY = RboundRect.height/2 +boundRect.y; }
imshow("frame", frame); //看看结果怎么样:> if(maxArea >5000)//如果最大轮廓的面积超过5000个像素,即认为他不是噪声,而是目标 {std::cout<<"midX= "<<midX<<" midY = "<<midY<<std::endl;//打印目标坐标。 //作者使用的分辨率为640*480,这一是有组于提高运算速度,二是方便截图写这稿子。//在这个分辨率下,我把摄像头的图像分成9个区域:/*
1
23
4
5
6
7
89
*///如过目标中心不在区域5内,则转动云台。比如当目标在区域2,即让云台抬头就好。如果目标在区域4,就要让云台向左转头。如果目标在区域1,那云台就要一边抬头,一边想左转头。当目标中心进入区域5内停止。//在作者的应用中,作者将区域5的大小定位100*100个像素。这对作者的霰弹枪流气动发射炮台足够了。//但是如果你想做个用狙击枪的炮台,这个瞄准精度可能就无法满足。所以你要做的就是在区域5里再画9个格子并重复一样的运算。或者你有能力用上光学变焦,那就JJBOMB SKY了。但是别忘了你的云台系统是否有最小移动距离,比如步进电机就有最小步长。 //另外作者弄了个简单的通信协议://a为电机顺时针转动//b为电机逆时针转动//c为电机停止//大小写区分左右转头电机,和仰俯电机。 if(midX > 370){Mega.SerialWrite('a');}//aelse if(midX <= 370&& midX >=270 ){Mega.SerialWrite('c');} // c//区域5为100像素长宽时,得出X轴的两条分界线在270 和370。 else {Mega.SerialWrite('b');} //b if(midY > 270){Mega.SerialWrite('B');}//Belse if(midY <=270&& midY >200){Mega.SerialWrite('C');}//C//区域5为100像素长宽时,得出X轴的两条分界线在200 和270。 else {Mega.SerialWrite('A');}//A}else {std::cout<<"TARGETTOO SMALL OR NOT FOUND"<<std::endl; //如果目标太小,就开始不断摆头进行搜索,直到发现满足条件的目标。 //左右转头电机的搜索timer1++;if(timer1 ==480){timer1=0;}if(timer1<240){Mega.SerialWrite('a');}if(timer1>=240){Mega.SerialWrite('b');} //仰俯电机的搜索timer2++;if(timer2==150){timer2=0;}if(timer2<75){Mega.SerialWrite('A');}if(timer2>=75){Mega.SerialWrite('B');} }
//最后在while循环里加上这句if(waitKey(30) >= 0)break;//等待按键,ms为等待时间,单位为毫秒。;//如果有按下ESC键便跳出,终止程序。//因为cvWaitKey()中有延迟功能,所以很多人使用这句来控制图像处理的频率。比如如果你的图像算法简单,运算时间可以忽略不计的话,那将ms设为30就能每30毫秒处理一幅图像,换而言之,1秒/30毫秒 = 33帧的图像。由于Opencv也有将图像存成影片格式的功能,所以通过改延迟就能很容易完成延迟摄影这类功能。
第三部分:通信 下面要做的就是将这两个数送往控制云台电机的系统。当然我知道很多人会想使用树莓派,PCDUINO,Cubieboard之类既有能力运行Opencv又自带GPIO的微型电脑板来一次完成全部功能。但是无奈作者一个都没玩过,所以暂时无能为力,日后等我玩会了,一定补上。
windows下通信:/***********************************分割线大人******主程序内*********************//初始化串口//这段放入main()即可 SerialXBEE;//声明串口 XBEE.SerialOpen("COM9",GENERIC_WRITE);//串口号和读写许可 XBEE.MessageParam();//设定串口 XBEE.CommTimeOut();
//这段用于发送数据,先发送x轴坐标,再发送轴y坐标,最后发送一个0提示下位机发送完毕。XBEE.SerialWrite(xdistance,ydistance,0);
/***********************************分割线君*****serial.h ******************#ifndef SERIAL_H#define SERIAL_H
#include<windows.h>//调用window驱动库#include <string>#include <iostream>
using namespace std;
class Serial{ public: void SerialOpen(LPCSTR, DWORD);//打开串口函数 void MessageParam();//消息函数 void CommTimeOut(); void SerialWrite(char);//发送一个CHAR变量到串口的函数 void SerialWrite(int, int, int);//发送3个INT变量到串口的函数 string SerialRead();//读取串口并将结构存入一个STRING HANDLE getHandle();//返回句柄
private: HANDLE HAND; DCB PARAMS; COMMTIMEOUTS timeouts;};
#endif//**********************************分割线酱******************************
//**********************************分割线大小姐*****serial.cpp*************************#include"Serial.h"
voidSerial::SerialOpen(LPCSTR port, DWORD func) //创建一个串口连接{ HAND=CreateFile(port, func, 0, 0, OPEN_EXISTING,FILE_ATTRIBUTE_NORMAL, 0); //Opensserial port and assigns a Handle if(HAND==INVALID_HANDLE_VALUE) //测试连接是否接通 { if(GetLastError()==ERROR_FILE_NOT_FOUND) { cout<<"failed1"<<endl; //如果串口连接不存在,报错 } cout<<"failed2"<<endl; //如果其他问题导致串口连接不正常,报错 }}
void Serial::MessageParam() //设置串口{ DCB param = {0}; PARAMS = param; //将串口设定存档 PARAMS.DCBlength=sizeof(PARAMS); if(!GetCommState(HAND, &PARAMS)) { cout<<"failed3"<<endl; //无法读取串口设定,报错 } PARAMS.BaudRate=CBR_115200; //设定波特率 115200 PARAMS.ByteSize=8; //设定字节长度为8 PARAMS.StopBits=ONESTOPBIT; //设定停止位 PARAMS.Parity=NOPARITY; //奇偶校验位 if(HAND==INVALID_HANDLE_VALUE) { if(GetLastError()==ERROR_FILE_NOT_FOUND) { cout<<"failed4"<<endl; //无法找到串口,报错. } cout<<"failed5"<<endl; //其他错误导致串口不正常,报错 }}
void Serial::CommTimeOut() //设定断线{ COMMTIMEOUTS time = {0}; timeouts=time; //在断线时的设定 timeouts.ReadIntervalTimeout=0; timeouts.ReadTotalTimeoutConstant=0; timeouts.ReadTotalTimeoutMultiplier=0; timeouts.WriteTotalTimeoutConstant=0; timeouts.WriteTotalTimeoutMultiplier=0;
if(!SetCommTimeouts(HAND,&timeouts)) { cout<<"failed6"<<endl; //无法读取,报错 }}
voidSerial::SerialWrite(char command) //向串口写入一个char{ DWORD dwWritten; char trans; trans=command; if(!WriteFile(HAND, trans, sizeof(trans),&dwWritten, NULL)) { cout<<"failed7"<<endl; }}
voidSerial::SerialWrite(int left, int right, int water) //向串口写入三个int{ DWORD dwWritten; char trans; trans=left; if(!WriteFile(HAND, trans, sizeof(trans),&dwWritten, NULL)) //写入第1个INT { cout<<"failed8"<<endl; } trans=right; if(!WriteFile(HAND, trans, sizeof(trans),&dwWritten, NULL)) //写入第2个INT { cout<<"failed9"<<endl; } trans=water; if(!WriteFile(HAND, trans, sizeof(trans),&dwWritten, NULL)) //写入第3个INT { cout<<"failed10"<<endl; }}
string Serial::SerialRead() //从串口读出数据{ string RX; //声明一个变量用来储存读入的数据 chartemp={'0'}; DWORDdwRead; for(int d=0; d<85; d++) //保持读入数据直到达到最大值 { if(!ReadFile(HAND, temp, 1,&dwRead, NULL)) //读入一个byte { cout<<"failed11"<<endl; } if(temp >' ') { RX+=temp; //放入RX中 } else { d=100; //如果读入一个空格则停止读入 } } return RX;}
HANDLE Serial::getHandle() //返回句柄{ return HAND;}//**********************************分割线OTAKU******************************
第三部分:发射机构。
常见的发射机构有三种:气压,弹簧,加速轮。 第一种是气压发射机构。它使用:一个高压气瓶作为发射动力。一截软管作为导气通道。一根铜管(或PVC管)作为炮管。一个电磁阀作为扳机。一个自行车轮胎气嘴作为加气口。另需要若干接头把各部分连起来。之间的各种缝隙可以使用从热熔胶到汽车补胎胶的各种胶水处理。最后你需要一个自行车打气筒(最好带压力计)来给气瓶充气。
如下图:
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image039.pngfile:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image041.jpg
这种发射装置的好处是出力大,射程远,口径可变,对弹药几乎没限制,作者用这个机构发射nerf弹,30米射程问题不大。缺点也很明显,一次发射后需要大约30秒-2分钟重新灌气。而且基本每个气压发射器都会漏气,而处理漏气的办法要嘛不可靠,要吗很贵。作者用的是汽车用那种灌入轮胎内部的补胎胶。但是还是会多少漏点气。另外充气也是很麻烦的一件事,一个自行车气筒还是挺重挺大的。如果用电动气泵的话很容易弄爆,千万不要用。一般充到3个大气压就足够完成射击了,充多不安全。保存时记得将气放完。
第二种是弹簧发射机构。这类基本都是通过拆解一把NERF玩具枪来完成的。NERF玩具枪种类繁多,作者就不介绍了。发射子弹一般只需要两个动作,上膛和抠扳机。这两个动作各使用一个舵机就好了。控制非常简单。这种发射装置的优点是弹药量大,通常都有6发以上。装填速度高,只要把子弹塞回去就好。缺点也很明显,子弹速度慢,射程短,子弹也仅限制于NERF弹。
各种正版善哉NERF枪:file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image043.jpg
https://www.google.com.hk/search?newwindow=1&safe=strict&es_sm=93&biw=1600&bih=799&tbm=isch&sa=1&q=NERF&oq=NERF&gs_l=img.3...0.0.1.157.0.0.0.0.0.0.0.0..0.0....0...1c..42.img..4.0.0.8s-1uNs_nGQ
第三种是加速轮发射机构。如图:file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image045.pngfile:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image047.png
作者这款拆自一款叫attacknid的遥控玩具。他带了一个12发的弹鼓。用两个小电机高速旋转,一左一右将子弹加速投掷出来。如果不想花钱买而自己仿制也可以简单。我们只需要2个电机负责给子弹加速,再用一个电机或螺杆不停的将弹药往发射电机推过去就行。 自制这种发射机构的优点是弹药量受限制小,口径可变,弹药外形只要是圆柱形就行,发射距离中,装填速度高,可以通过PWM调节发射电机转速控制出膛速度。结构大致如下:
file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image048.png缺点是买的话不便宜,做的话需要比其他两种办法多花点时间,而且在电池没电时非常蛋疼。file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image050.pngattacknind遥控蜘蛛。
各种发射机构使用常见配置时横向比较:
气压弹射电机加速
弹夹小中-大没啥限制
射速超级慢慢快-极快
射程远中中
装填速度慢快快
弹药限制小只能用NERF小
自制难易度中中难
自制价格低中中
命中精度高糟糕高
属性伤害无无无
暴击率0.5%-10%0%
暴击伤害加成1%-10%0%
当然我知道有人会做磁轨发射、线圈发射甚至高压气体发射,不过那些太危险了。本文不讨论。 第四部分:云台控制云台电机数来数去也就只有三种可能:直流,步进,舵机。
这三种电机的程序电路已经在无数地方出现了无数次,所以这里只比较他们的优劣,不讨论如果使用。下表只作用于通常情况。
直流电机减速直流电机步进舵机
速度高低中-高中-高
力矩中中高高
精度低高高糟糕
电路复杂度低低高非常低
程序复杂度低低高非常低
价格高中中中
谢谢您看完全文。作者的哨戒炮项目因比赛内容变更被砍。因经费断饮,最终只做出一款样机(再次感谢神队友Ryan和Aaron):file:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image052.jpgfile:///C:/Users/lee/AppData/Local/Temp/msohtmlclip1/01/clip_image054.jpg
完 要是需要大云台的话可以试试二手高速球机 不带摄像头 发射装置可以用遥控坦克的BB蛋发射枪 真极客呀。{:soso_e179:} 必须加精! 机械部分的配件有买的地方吗? conquester 发表于 2014-5-6 08:56
机械部分的配件有买的地方吗?
都有 但是得麻烦你自己找,我告诉你 你拿去乌克兰,我就惨了 Lee 发表于 2014-5-7 12:51
都有 但是得麻烦你自己找,我告诉你 你拿去乌克兰,我就惨了
正想找毛子练练:) 顶作者,我的思路是用步进电机控制左轮,步进电机转1.8°为一发弹药打出去。 等我发工资了,我也去买器件回来弄一个看门,比狗好多了;P 牛:):):):):):):):)
页:
[1]
2