1 前提说明
个人并不是在校学生,在机缘巧合下指导学生做比赛,思路可能不成熟,没有经过太大批量的测试,但是能跑。仅是个人愚见,大家理性参考,欢迎提出建议。
2 解题思路
使用OpenMV或者K210又或者能够跑OpenCV的设备也可以做屏幕和红蓝点以及矩形的检测,对于整个题目,我们只需要屏幕四角坐标,A4纸四角坐标,红点位置坐标。可以直接使用OpenMV/K210等驱动云台,也可以将坐标信息发送到云台的控制板做单独的控制。
算法思路:先找到激光点位置与舵机角度的关系,然后根据红色激光笔和目标点位做X和Y方向的差值,并计算两个差值的比例关系,得到斜率k,这样就可以计算X和Y上移动的速度关系,当然也可以根据其他的数学关系让光点跑圆形等图形。
3 OpenMV代码
OpenMV在识别1mm的细线时识别率太低,所以做成了屏幕最中间的一个矩形,根据实际场地用摄像头内的矩形与场地上的50*50矩形重合即可。
数据部分通过串口发送各个关键坐标信息出去,20ms发送一次。
import sensor, image, time, math, pyb
from pyb import LED, UART
#led灯的定义
LedRed = LED(1)
LedGreen = LED(2)
LedBlue = LED(3)
sensor.reset()
sensor.set_pixformat(sensor.RGB565) # 灰度更快(160x120 max on OpenMV-M7)
sensor.set_framesize(sensor.QQVGA)
sensor.skip_frames(time = 2000)
sensor.set_auto_whitebal(False)
clock = time.clock()
uart = UART(3, 115200, timeout_char=1000)
uart.init(115200, bits=8, parity=None, stop=1, timeout_char=1000)
#定义红色激光点的颜色阈值
#red_threshold = (48, 88, 4, 41, -4, 32)
#red_threshold = (65, 91, 0, 22, 3, 15)
#red_threshold = (68, 91, -4, 17, 6, 18)
red_threshold = (73, 64, 0, 17, 6, 15)
#green_threshold = (80, 100, -69, 9, -10, 40)
#定义外框的像素宽度大小以及左上角xy坐标
exteRectW = 86
exteRectX = int((160-exteRectW)/2)
exteRectY = int((120-exteRectW)/2)
#外框周边四角坐标,从左上角顺时针为顺序
exteRect = [[exteRectX,exteRectY],[exteRectX+exteRectW,exteRectY],\
[exteRectX+exteRectW,exteRectY+exteRectW],[exteRectX,exteRectY+exteRectW]]
#外框区域ROI
exteROI = [exteRectX,exteRectY,exteRectW,exteRectW]
#定义框内查询ROI
searchDif = 5
inteSearchROI = [exteRectX+searchDif,exteRectY+searchDif,exteRectW-searchDif,exteRectW-searchDif]
inteRect = [[0,0],[0,0],[0,0],[0,0]] #内框周边四角坐标
#定义红色与绿色激光点的位置
redPoint = [0,0]
greenPoint = [0,0]
#整合数据、串口发送
#帧头 内部矩形四角坐标 红色光点坐标 绿色光电坐标 校验和
def com_sendbyte():
x0h = (inteRect[0][0] & 0xFF00) >> 8
x0l = inteRect[0][0] & 0x00FF
y0h = (inteRect[0][1] & 0xFF00) >> 8
y0l = inteRect[0][1] & 0x00FF
x1h = (inteRect[1][0] & 0xFF00) >> 8
x1l = inteRect[1][0] & 0x00FF
y1h = (inteRect[1][1] & 0xFF00) >> 8
y1l = inteRect[1][1] & 0x00FF
x2h = (inteRect[2][0] & 0xFF00) >> 8
x2l = inteRect[2][0] & 0x00FF
y2h = (inteRect[2][1] & 0xFF00) >> 8
y2l = inteRect[2][1] & 0x00FF
x3h = (inteRect[3][0] & 0xFF00) >> 8
x3l = inteRect[3][0] & 0x00FF
y3h = (inteRect[3][1] & 0xFF00) >> 8
y3l = inteRect[3][1] & 0x00FF
rxh = (redPoint[0] & 0xFF00) >> 8
rxl = redPoint[0] & 0x00FF
ryh = (redPoint[1] & 0xFF00) >> 8
ryl = redPoint[1] & 0x00FF
gxh = (greenPoint[0] & 0xFF00) >> 8
gxl = greenPoint[0] & 0x00FF
gyh = (greenPoint[1] & 0xFF00) >> 8
gyl = greenPoint[1] & 0x00FF
check = x0h + x0l + y0h + y0l + x1h + x1l + y1h + y1l + x2h + x2l + y2h + y2l + \
x3h + x3l + y3h + y3l + rxh + rxl + ryh + ryl + gxh + gxl + gyh + gyl
print(x0h , x0l , y0h , y0l , x1h , x1l , y1h , y1l , x2h , x2l , y2h , y2l , \
x3h , x3l , y3h , y3l , rxh , rxl , ryh , ryl , gxh , gxl , gyh , gyl, check)
arr = bytearray([x0h , x0l , y0h , y0l , x1h , x1l , y1h , y1l , x2h , x2l , y2h , y2l , \
x3h , x3l , y3h , y3l , rxh , rxl , ryh , ryl , gxh , gxl , gyh , gyl, check])
uart.write(arr)
def find_max(blobs):
max_size=0
for blob in blobs:
if blob[2]*blob[3] > max_size:
max_blob=blob
max_size = blob[2]*blob[3]
return max_blob
sendtime = pyb.millis()
while(True):
clock.tick()
img = sensor.snapshot().lens_corr(strength = 1.2, zoom = 1.2)
#img = sensor.snapshot()
#1 先在屏幕中央绘制外框ROI,比赛时可用于调试摄像头位置
img.draw_rectangle(exteROI, color = (255, 255, 0))
img.draw_cross(80, 60, color = (255, 255, 0), size=3)
#for ep in exteRect: img.draw_circle(ep[0], ep[1], 3, color = (255, 255, 0))
#2 根据外框ROI查询内框
for r in img.find_rects(roi=inteSearchROI,threshold = 12000):
for ip in r.corners(): img.draw_circle(ip[0], ip[1], 3, color = (255, 255, 0))
for i in range(4): inteRect[i] = r.corners()[i]
print(inteRect)
#寻找红色激光点
blobs = img.find_blobs([red_threshold])
if blobs:
max_blob = find_max(blobs)
#img.draw_rectangle(max_blob[0:4], color = (255, 255, 0)) # rect
img.draw_cross(max_blob[5], max_blob[6], color = (255, 255, 0)) # cx, cy
print("找到了:", max_blob[5], max_blob[6])
redPoint[0] = max_blob[5]
redPoint[1] = max_blob[6]
else:
print("未找到")
if pyb.millis() - sendtime > 20:#20ms发送一次数据到单片机
sendtime = pyb.millis()
com_sendbyte()
4 单片机部分代码(STM32)
在单片机中我们首先实现舵机控制代码,然后实现串口接收数据并解析数据代码。我是直接在串口接收处理函数中进行舵机的运行控制。
4.1 舵机代码
void Motor_Init(void)
{
RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM5, ENABLE);
RCC_APB2PeriphClockCmd(RCC_APB2Periph_GPIOA | RCC_APB2Periph_AFIO, ENABLE);
GPIO_InitTypeDef GPIO_InitStructure;
GPIO_InitStructure.GPIO_Pin = GPIO_Pin_0 | GPIO_Pin_1;
GPIO_InitStructure.GPIO_Mode = GPIO_Mode_AF_PP;
GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
GPIO_Init(GPIOA, &GPIO_InitStructure);
TIM_TimeBaseInitTypeDef TIM_TimeBaseStructure;
TIM_OCInitTypeDef TIM_OCInitStructure;
TIM_TimeBaseStructure.TIM_Period = 20000-1;//重装载值,20ms,50hz
TIM_TimeBaseStructure.TIM_Prescaler = 72-1;
TIM_TimeBaseStructure.TIM_ClockDivision = 0;
TIM_TimeBaseStructure.TIM_CounterMode = TIM_CounterMode_Up;
TIM_TimeBaseInit(TIM5, &TIM_TimeBaseStructure);
TIM_OCInitStructure.TIM_OCMode = TIM_OCMode_PWM1;
TIM_OCInitStructure.TIM_OutputState = TIM_OutputState_Enable;
TIM_OCInitStructure.TIM_Pulse = 1500;
TIM_OCInitStructure.TIM_OCPolarity = TIM_OCPolarity_High;
TIM_OC1Init(TIM5, &TIM_OCInitStructure);
TIM_OC2Init(TIM5, &TIM_OCInitStructure);
TIM_OC1PreloadConfig(TIM5, TIM_OCPreload_Enable);
TIM_OC2PreloadConfig(TIM5, TIM_OCPreload_Enable);
TIM_ARRPreloadConfig(TIM5, ENABLE);
TIM_Cmd(TIM5, ENABLE);
}
void Motor_SetDJ_PWM(u8 djnum, float pwm)
{
switch(djnum)
{
case 1:TIM_SetCompare1(TIM5, pwm);break;
case 2:TIM_SetCompare2(TIM5, pwm);break;
}
}
uint16_t Motor_ReadDJ_PWM(u8 djnum)
{
uint16_t value = 0;
switch(djnum)
{
case 1:value = TIM_GetCapture1(TIM5);break;
case 2:value = TIM_GetCapture2(TIM5);break;
}
return value;
}
4.2 运动控制头文件中定义内容
typedef enum {
INIT_STATE = 0,//找原点
EXTE0_STATE = 1,//找外框的第一个坐标点
EXTE1_STATE = 2,//找外框的第二个坐标点
EXTE2_STATE = 3,//找外框的第三个坐标点
EXTE3_STATE = 4,//找外框的第四个坐标点
INTE0_STATE = 5,//找内框的第一个坐标点
INTE1_STATE = 6,//找内框的第二个坐标点
INTE2_STATE = 7,//找内框的第三个坐标点
INTE3_STATE = 8,//找内框的第四个坐标点
GREEN_STATE = 9,//绿色找红色
}sysStateDef;
#define OPENMV_MAXSIZE 128
typedef struct{
u8 openmv_rxbuff[OPENMV_MAXSIZE];//接收缓存区
u8 openmv_rxcount;
u8 openmv_rxover;
}OpenMV_TypeDef;
typedef struct{
uint16_t inteRect[4][2];
uint16_t redPoint[2];
uint16_t greenPoint[2];
uint8_t state;
}Taget_TypeDef;
//定义外框的像素宽度大小以及左上角xy坐标
#define exteRectW 86 //宽度要根据实际摄像头测量到的50*50矩形大小像素点对应
#define exteRectX ((160-exteRectW)/2.0) //左上角X坐标
#define exteRectY ((120-exteRectW)/2.0) //左上角Y坐标
#define exteCenterX 80 //中心点X坐标
#define exteCenterY 60 //中心点Y坐标
#define PUL_R 1225 //右限
#define PUL_L 1580 //左限
#define PUL_U 1130 //上限
#define PUL_D 1365 //下限
#define abs(x) ((x)<0?(-(x)):(x))
#define INC_X ((PUL_L-PUL_R)/exteRectW/2)
#define INC_Y ((PUL_D-PUL_U)/exteRectW/2)
4.3 数据解析与运动控制核心代码
OpenMV_TypeDef openmv = {0};
//中断 USART1
void USART1_IRQHandler(void)
{
//如果没有接收完成,则缓存数据
if(USART_GetFlagStatus(USART1, USART_FLAG_RXNE) == SET && openmv.openmv_rxover == 0)
{
USART_ClearFlag(USART1, USART_FLAG_RXNE);
openmv.openmv_rxbuff[openmv.openmv_rxcount++] = USART_ReceiveData(USART1);
openmv.openmv_rxcount %= OPENMV_MAXSIZE;//防止溢出
}
//空闲中断:接收完成
if(USART_GetFlagStatus(USART1, USART_FLAG_IDLE) == SET)
{
u32 state = USART1->SR;
state = USART1->DR; //清标志
openmv.openmv_rxover = 1; //接收完成标志位
}
}
//外框周边四角坐标,从左上角顺时针为顺序
uint16_t exteRect[4][2] = {{exteRectX, exteRectY},
{exteRectX+exteRectW, exteRectY},
{exteRectX+exteRectW, exteRectY+exteRectW},
{exteRectX, exteRectY+exteRectW}};
//定义一个红色的目标点,用于引导红色激光笔移动
uint16_t rTarPoint[2] = {exteCenterX, exteCenterY};
//定义一个绿色的目标点,用于引导绿色激光笔移动
uint16_t gTarPoint[2] = {exteCenterX, exteCenterY};
//openmv数据解析函数
Taget_TypeDef taget = {0};
float pulseX = 0, incX = INC_X;
float pulseY = 0, incY = INC_Y;
float k = 1;//X差值对于Y差值的比例关系,用于移动时的协同
void OpenMV_Analysis(void)
{
uint8_t check = 0, i = 0, j = 0;
if(openmv.openmv_rxover == 0) return; //保证接收完成才解析
for(i=0; i<openmv.openmv_rxcount; i++) {
printf("%d ", openmv.openmv_rxbuff[i]);
}
printf("\r\n");
for(i=0; i<openmv.openmv_rxcount-1; i++) {
check += openmv.openmv_rxbuff[i];
}
if(check == openmv.openmv_rxbuff[openmv.openmv_rxcount-1]) {
printf("校验正确\r\n");
taget.inteRect[0][0] = (openmv.openmv_rxbuff[0]<<8) + openmv.openmv_rxbuff[1];
taget.inteRect[0][1] = (openmv.openmv_rxbuff[2]<<8) + openmv.openmv_rxbuff[3];
taget.inteRect[1][0] = (openmv.openmv_rxbuff[4]<<8) + openmv.openmv_rxbuff[5];
taget.inteRect[1][1] = (openmv.openmv_rxbuff[6]<<8) + openmv.openmv_rxbuff[7];
taget.inteRect[2][0] = (openmv.openmv_rxbuff[8]<<8) + openmv.openmv_rxbuff[9];
taget.inteRect[2][1] = (openmv.openmv_rxbuff[10]<<8) + openmv.openmv_rxbuff[11];
taget.inteRect[3][0] = (openmv.openmv_rxbuff[12]<<8) + openmv.openmv_rxbuff[13];
taget.inteRect[3][1] = (openmv.openmv_rxbuff[14]<<8) + openmv.openmv_rxbuff[15];
taget.redPoint[0] = (openmv.openmv_rxbuff[16]<<8) + openmv.openmv_rxbuff[17];
taget.redPoint[1] = (openmv.openmv_rxbuff[18]<<8) + openmv.openmv_rxbuff[19];
taget.greenPoint[0] = (openmv.openmv_rxbuff[20]<<8) + openmv.openmv_rxbuff[21];
taget.greenPoint[1] = (openmv.openmv_rxbuff[22]<<8) + openmv.openmv_rxbuff[23];
printf("内部矩形坐标:");
for(i=0; i<4; i++) {
for(j=0; j<2; j++) {
printf("%d ", taget.inteRect[i][j]);
}
}
printf("\r\n");
printf("红色激光点坐标:%d %d\r\n", taget.redPoint[0], taget.redPoint[1]);
printf("绿色激光点坐标:%d %d\r\n", taget.greenPoint[0], taget.greenPoint[1]);
//根据当前状态更新目标位置
printf("S:%d abs:%d %d\r\n", taget.state, abs(taget.redPoint[0]-rTarPoint[0]), abs(taget.redPoint[1]-rTarPoint[1]));
if(abs(taget.redPoint[0]-rTarPoint[0]) < 4 && abs(taget.redPoint[1]-rTarPoint[1]) < 4) {
taget.state++;
taget.state %= 5;
}
switch(taget.state) {
case INIT_STATE: rTarPoint[0] = exteCenterX; rTarPoint[1] = exteCenterY; break;
case EXTE0_STATE: rTarPoint[0] = exteRect[0][0]; rTarPoint[1] = exteRect[0][1]; break;
case EXTE1_STATE: rTarPoint[0] = exteRect[1][0]; rTarPoint[1] = exteRect[1][1]; break;
case EXTE2_STATE: rTarPoint[0] = exteRect[2][0]; rTarPoint[1] = exteRect[2][1]; break;
case EXTE3_STATE: rTarPoint[0] = exteRect[3][0]; rTarPoint[1] = exteRect[3][1]; break;
}
printf("红色激光点目标:%d %d\r\n", rTarPoint[0], rTarPoint[1]);
k = (rTarPoint[0] - taget.redPoint[0])/(rTarPoint[1] - taget.redPoint[1]);
k = abs(k);
incX = k*INC_X;
printf("%0.2f\r\n", incX);
if(taget.redPoint[0] != 0)
{
pulseX = Arm_ReadDJ_PWM(DUOJI1_P2_X);
if(abs(taget.redPoint[0]-rTarPoint[0]) > 3) {
if(taget.redPoint[0]-rTarPoint[0] > 3) {
pulseX += incX;
}
else if(taget.redPoint[0]-rTarPoint[0] < -3) {
pulseX -= incX;
}
pulseX = (pulseX<(PUL_R-5))?(PUL_R-5):((pulseX>(PUL_L+5))?(PUL_L+5):pulseX);
Arm_SetDJ_PWM(DUOJI1_P2_X, pulseX);
}
}
if(taget.redPoint[1] != 0)
{
pulseY = Arm_ReadDJ_PWM(DUOJI2_P3_Y);
if(abs(taget.redPoint[1]-rTarPoint[1]) > 3) {
if(taget.redPoint[1]-rTarPoint[1] > 3) {
pulseY -= incY;
}
else if(taget.redPoint[1]-rTarPoint[1] < -3) {
pulseY += incY;
}
1100 上限 下限 1350
pulseY = (pulseY<(PUL_U-5))?(PUL_U-5):((pulseY>(PUL_D+5))?(PUL_D+5):pulseY);
Arm_SetDJ_PWM(DUOJI2_P3_Y, pulseY);
}
}
}
else {
printf("校验失败\r\n");
}
memset(&openmv, 0, sizeof(openmv));//执行完一次之后清理数据
}